Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:20:28 +08:00
commit b727790a9e
65 changed files with 16412 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env bash
# ============================================================================
# Field Checker Script
# ============================================================================
# Purpose: Verify required and recommended fields in plugin/marketplace configs
# Version: 1.0.0
# Usage: ./field-checker.sh <config-file> <type> [strict]
# Returns: 0=all required present, 1=missing required, 2=error
# ============================================================================
set -euo pipefail
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../../scripts/validate-lib.sh"
# ====================
# Configuration
# ====================
readonly CONFIG_FILE="${1:-}"
readonly TYPE="${2:-}"
readonly STRICT="${3:-false}"
# ====================
# Field Definitions
# ====================
# Plugin required fields
PLUGIN_REQUIRED_FIELDS=(
"name"
"version"
"description"
"author"
"license"
)
# Plugin recommended fields
PLUGIN_RECOMMENDED_FIELDS=(
"repository"
"homepage"
"keywords"
"category"
)
# Marketplace required fields
MARKETPLACE_REQUIRED_FIELDS=(
"name"
"owner"
"owner.name"
"owner.email"
"plugins"
)
# Marketplace recommended fields
MARKETPLACE_RECOMMENDED_FIELDS=(
"version"
"metadata.description"
"metadata.homepage"
"metadata.repository"
)
# ====================
# Validation Functions
# ====================
check_field_exists() {
local file="$1"
local field="$2"
local value
# Use json_get from validate-lib.sh
value=$(json_get "${file}" ".${field}")
if [[ -n "${value}" && "${value}" != "null" ]]; then
return 0
else
return 1
fi
}
get_field_value() {
local file="$1"
local field="$2"
json_get "${file}" ".${field}"
}
check_field_empty() {
local value="$1"
if [[ -z "${value}" || "${value}" == "null" || "${value}" == '""' || "${value}" == "[]" || "${value}" == "{}" ]]; then
return 0 # Is empty
else
return 1 # Not empty
fi
}
# ====================
# Plugin Validation
# ====================
validate_plugin_fields() {
local file="$1"
local strict="$2"
local missing_required=0
local missing_recommended=0
print_section "Required Fields (${#PLUGIN_REQUIRED_FIELDS[@]})"
for field in "${PLUGIN_REQUIRED_FIELDS[@]}"; do
if check_field_exists "${file}" "${field}"; then
local value
value=$(get_field_value "${file}" "${field}")
# Check if empty
if check_field_empty "${value}"; then
print_error "${field}: Present but empty (REQUIRED)"
((missing_required++))
else
# Truncate long values for display
if [[ "${#value}" -gt 50 ]]; then
value="${value:0:47}..."
fi
print_success "${field}: \"${value}\""
fi
else
print_error "${field}: Missing (REQUIRED)"
((missing_required++))
fi
done
echo ""
print_section "Recommended Fields (${#PLUGIN_RECOMMENDED_FIELDS[@]})"
for field in "${PLUGIN_RECOMMENDED_FIELDS[@]}"; do
if check_field_exists "${file}" "${field}"; then
print_success "${field}: Present"
else
print_warning "${field}: Missing (improves quality)"
((missing_recommended++))
fi
done
echo ""
# Summary
if [[ ${missing_required} -eq 0 ]]; then
print_success "All required fields present"
else
print_error "Missing ${missing_required} required field(s)"
fi
if [[ ${missing_recommended} -gt 0 ]]; then
print_info "Missing ${missing_recommended} recommended field(s)"
fi
# Determine exit code
if [[ ${missing_required} -gt 0 ]]; then
return 1
elif [[ "${strict}" == "true" && ${missing_recommended} -gt 0 ]]; then
print_warning "Strict mode: Recommended fields are required"
return 1
else
return 0
fi
}
# ====================
# Marketplace Validation
# ====================
validate_marketplace_fields() {
local file="$1"
local strict="$2"
local missing_required=0
local missing_recommended=0
print_section "Required Fields (${#MARKETPLACE_REQUIRED_FIELDS[@]})"
for field in "${MARKETPLACE_REQUIRED_FIELDS[@]}"; do
if check_field_exists "${file}" "${field}"; then
local value
value=$(get_field_value "${file}" "${field}")
# Special handling for plugins array
if [[ "${field}" == "plugins" ]]; then
local count
count=$(get_json_array_length "${file}" ".plugins")
if [[ ${count} -gt 0 ]]; then
print_success "${field}: Array with ${count} entries"
else
print_error "${field}: Present but empty (REQUIRED)"
((missing_required++))
fi
elif check_field_empty "${value}"; then
print_error "${field}: Present but empty (REQUIRED)"
((missing_required++))
else
# Truncate long values
if [[ "${#value}" -gt 50 ]]; then
value="${value:0:47}..."
fi
print_success "${field}: \"${value}\""
fi
else
print_error "${field}: Missing (REQUIRED)"
((missing_required++))
fi
done
echo ""
print_section "Recommended Fields (${#MARKETPLACE_RECOMMENDED_FIELDS[@]})"
for field in "${MARKETPLACE_RECOMMENDED_FIELDS[@]}"; do
if check_field_exists "${file}" "${field}"; then
print_success "${field}: Present"
else
print_warning "${field}: Missing (improves quality)"
((missing_recommended++))
fi
done
echo ""
# Summary
if [[ ${missing_required} -eq 0 ]]; then
print_success "All required fields present"
else
print_error "Missing ${missing_required} required field(s)"
fi
if [[ ${missing_recommended} -gt 0 ]]; then
print_info "Missing ${missing_recommended} recommended field(s)"
fi
# Determine exit code
if [[ ${missing_required} -gt 0 ]]; then
return 1
elif [[ "${strict}" == "true" && ${missing_recommended} -gt 0 ]]; then
print_warning "Strict mode: Recommended fields are required"
return 1
else
return 0
fi
}
# ====================
# Main Logic
# ====================
main() {
# Validate arguments
if [[ -z "${CONFIG_FILE}" ]]; then
print_error "Usage: $0 <config-file> <type> [strict]"
print_info "Types: plugin, marketplace"
exit 2
fi
if [[ ! -f "${CONFIG_FILE}" ]]; then
print_error "Configuration file not found: ${CONFIG_FILE}"
exit 2
fi
if [[ -z "${TYPE}" ]]; then
print_error "Type required: plugin or marketplace"
exit 2
fi
# Validate JSON syntax first
if ! validate_json_syntax "${CONFIG_FILE}"; then
print_error "Invalid JSON syntax in ${CONFIG_FILE}"
print_info "Run JSON validation first to fix syntax errors"
exit 2
fi
# Print header
print_header "Required Fields Validation"
echo "Target: ${CONFIG_FILE}"
echo "Type: ${TYPE}"
echo "Strict Mode: ${STRICT}"
echo ""
# Validate based on type
case "${TYPE}" in
plugin)
if validate_plugin_fields "${CONFIG_FILE}" "${STRICT}"; then
echo ""
print_header "✅ PASS: All required fields present"
exit 0
else
echo ""
print_header "❌ FAIL: Missing required fields"
# Show remediation
echo ""
print_info "Action Required:"
echo " Add missing required fields to ${CONFIG_FILE}"
echo " Refer to plugin schema: .claude/docs/plugins/plugins-reference.md"
exit 1
fi
;;
marketplace)
if validate_marketplace_fields "${CONFIG_FILE}" "${STRICT}"; then
echo ""
print_header "✅ PASS: All required fields present"
exit 0
else
echo ""
print_header "❌ FAIL: Missing required fields"
# Show remediation
echo ""
print_info "Action Required:"
echo " Add missing required fields to ${CONFIG_FILE}"
echo " Refer to marketplace schema: .claude/docs/plugins/plugin-marketplaces.md"
exit 1
fi
;;
*)
print_error "Unknown type: ${TYPE}"
print_info "Valid types: plugin, marketplace"
exit 2
;;
esac
}
main "$@"

View File

@@ -0,0 +1,448 @@
#!/usr/bin/env python3
# ============================================================================
# Format Validator Script
# ============================================================================
# Purpose: Validate format compliance for semver, URLs, emails, naming
# Version: 1.0.0
# Usage: ./format-validator.py --file <path> --type <plugin|marketplace> [--strict]
# Returns: 0=all valid, 1=format violations, 2=error
# ============================================================================
import json
import sys
import argparse
import re
from pathlib import Path
from typing import Dict, List, Tuple, Optional
# ====================
# Color Support
# ====================
class Colors:
"""ANSI color codes for terminal output"""
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
CYAN = '\033[0;36m'
BOLD = '\033[1m'
NC = '\033[0m'
@classmethod
def disable(cls):
"""Disable colors for non-TTY output"""
cls.RED = cls.GREEN = cls.YELLOW = cls.BLUE = cls.CYAN = cls.BOLD = cls.NC = ''
if not sys.stdout.isatty():
Colors.disable()
# ====================
# Format Patterns
# ====================
# Semantic versioning: X.Y.Z
SEMVER_PATTERN = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$')
# Lowercase-hyphen naming: plugin-name
LOWERCASE_HYPHEN_PATTERN = re.compile(r'^[a-z0-9]+(-[a-z0-9]+)*$')
# Email: RFC 5322 simplified
EMAIL_PATTERN = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
# URL: http or https
URL_PATTERN = re.compile(r'^https?://')
# HTTPS only
HTTPS_PATTERN = re.compile(r'^https://')
# SPDX License Identifiers (common ones)
SPDX_LICENSES = [
'MIT', 'Apache-2.0', 'GPL-3.0', 'GPL-2.0', 'LGPL-3.0', 'LGPL-2.1',
'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', 'AGPL-3.0',
'Unlicense', 'CC0-1.0', 'Proprietary'
]
# Approved categories (10 standard)
APPROVED_CATEGORIES = [
'development', 'testing', 'deployment', 'documentation', 'security',
'database', 'monitoring', 'productivity', 'quality', 'collaboration'
]
# ====================
# Validation Functions
# ====================
class FormatValidator:
"""Format validation logic"""
def __init__(self, strict_https: bool = False):
self.strict_https = strict_https
self.errors: List[Tuple[str, str, str]] = []
self.warnings: List[Tuple[str, str]] = []
self.passed: List[Tuple[str, str]] = []
def validate_semver(self, field: str, value: str) -> bool:
"""Validate semantic versioning"""
if not value:
return True # Skip empty (handled by required fields check)
if SEMVER_PATTERN.match(value):
self.passed.append((field, f'"{value}" (semver)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must use semantic versioning (X.Y.Z)\n'
' Pattern: MAJOR.MINOR.PATCH\n'
' Example: 1.0.0, 2.1.5'
)
self.errors.append(error)
return False
def validate_lowercase_hyphen(self, field: str, value: str) -> bool:
"""Validate lowercase-hyphen naming"""
if not value:
return True
if LOWERCASE_HYPHEN_PATTERN.match(value):
self.passed.append((field, f'"{value}" (lowercase-hyphen)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must use lowercase-hyphen format\n'
' Pattern: ^[a-z0-9]+(-[a-z0-9]+)*$\n'
' Example: my-plugin, test-tool, plugin123'
)
self.errors.append(error)
return False
def validate_email(self, field: str, value: str) -> bool:
"""Validate email address"""
if not value:
return True
if EMAIL_PATTERN.match(value):
self.passed.append((field, f'"{value}" (valid email)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must be valid email address\n'
' Pattern: user@domain.tld\n'
' Example: developer@example.com'
)
self.errors.append(error)
return False
def validate_url(self, field: str, value: str) -> bool:
"""Validate URL format"""
if not value:
return True
if self.strict_https and not HTTPS_PATTERN.match(value):
error = (
field,
f'"{value}"',
'Invalid: HTTPS required in strict mode\n'
f' Current: {value}\n'
f' Required: {value.replace("http://", "https://", 1)}'
)
self.errors.append(error)
return False
elif URL_PATTERN.match(value):
if value.startswith('http://'):
self.warnings.append((
field,
f'"{value}" - Consider using HTTPS for security'
))
self.passed.append((field, f'"{value}" (valid URL)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must be valid URL\n'
' Pattern: https://domain.tld/path\n'
' Example: https://github.com/user/repo'
)
self.errors.append(error)
return False
def validate_license(self, field: str, value: str) -> bool:
"""Validate SPDX license identifier"""
if not value:
return True
if value in SPDX_LICENSES:
self.passed.append((field, f'"{value}" (SPDX identifier)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must be SPDX license identifier\n'
' Common: MIT, Apache-2.0, GPL-3.0, BSD-3-Clause, ISC\n'
' See: https://spdx.org/licenses/'
)
self.errors.append(error)
return False
def validate_category(self, field: str, value: str) -> bool:
"""Validate category against approved list"""
if not value:
return True
if value in APPROVED_CATEGORIES:
self.passed.append((field, f'"{value}" (approved category)'))
return True
else:
error = (
field,
f'"{value}"',
'Invalid: Must be one of 10 approved categories\n'
' Valid: development, testing, deployment, documentation,\n'
' security, database, monitoring, productivity,\n'
' quality, collaboration'
)
self.errors.append(error)
return False
def validate_description_length(self, field: str, value: str) -> bool:
"""Validate description length (50-200 chars recommended)"""
if not value:
return True
length = len(value)
if 50 <= length <= 200:
self.passed.append((field, f'Valid length ({length} chars)'))
return True
elif length < 50:
self.warnings.append((
field,
f'Short description ({length} chars) - consider 50-200 characters for clarity'
))
return True
else:
self.warnings.append((
field,
f'Long description ({length} chars) - consider keeping under 200 characters'
))
return True
# ====================
# Plugin Validation
# ====================
def validate_plugin_formats(data: Dict, validator: FormatValidator) -> int:
"""Validate plugin format compliance"""
print(f"{Colors.CYAN}Format Checks:{Colors.NC}\n")
# name: lowercase-hyphen
if 'name' in data:
validator.validate_lowercase_hyphen('name', data['name'])
# version: semver
if 'version' in data:
validator.validate_semver('version', data['version'])
# description: length check
if 'description' in data:
validator.validate_description_length('description', data['description'])
# license: SPDX
if 'license' in data:
validator.validate_license('license', data['license'])
# homepage: URL
if 'homepage' in data:
validator.validate_url('homepage', data['homepage'])
# repository: URL or object
if 'repository' in data:
repo = data['repository']
if isinstance(repo, str):
validator.validate_url('repository', repo)
elif isinstance(repo, dict) and 'url' in repo:
validator.validate_url('repository.url', repo['url'])
# category: approved list
if 'category' in data:
validator.validate_category('category', data['category'])
# author: email if object
if 'author' in data:
author = data['author']
if isinstance(author, dict) and 'email' in author:
validator.validate_email('author.email', author['email'])
return 0 if not validator.errors else 1
# ====================
# Marketplace Validation
# ====================
def validate_marketplace_formats(data: Dict, validator: FormatValidator) -> int:
"""Validate marketplace format compliance"""
print(f"{Colors.CYAN}Format Checks:{Colors.NC}\n")
# name: lowercase-hyphen
if 'name' in data:
validator.validate_lowercase_hyphen('name', data['name'])
# owner.email: email
if 'owner' in data and isinstance(data['owner'], dict):
if 'email' in data['owner']:
validator.validate_email('owner.email', data['owner']['email'])
# version: semver (if present)
if 'version' in data:
validator.validate_semver('version', data['version'])
# metadata fields
if 'metadata' in data and isinstance(data['metadata'], dict):
metadata = data['metadata']
if 'description' in metadata:
validator.validate_description_length('metadata.description', metadata['description'])
if 'homepage' in metadata:
validator.validate_url('metadata.homepage', metadata['homepage'])
if 'repository' in metadata:
validator.validate_url('metadata.repository', metadata['repository'])
return 0 if not validator.errors else 1
# ====================
# Output Formatting
# ====================
def print_results(validator: FormatValidator):
"""Print validation results"""
print()
# Passed checks
if validator.passed:
for field, msg in validator.passed:
print(f" {Colors.GREEN}{field}: {msg}{Colors.NC}")
# Errors
if validator.errors:
print()
for field, value, msg in validator.errors:
print(f" {Colors.RED}{field}: {value}{Colors.NC}")
for line in msg.split('\n'):
print(f" {line}")
print()
# Warnings
if validator.warnings:
print()
for field, msg in validator.warnings:
print(f" {Colors.YELLOW}⚠️ {field}: {msg}{Colors.NC}")
# Summary
print()
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
total = len(validator.passed) + len(validator.errors)
passed_count = len(validator.passed)
if validator.errors:
print(f"{Colors.RED}Failed: {len(validator.errors)}{Colors.NC}")
if validator.warnings:
print(f"{Colors.YELLOW}Warnings: {len(validator.warnings)}{Colors.NC}")
print(f"Status: {Colors.RED}FAIL{Colors.NC}")
else:
print(f"Passed: {passed_count}/{total}")
if validator.warnings:
print(f"{Colors.YELLOW}Warnings: {len(validator.warnings)}{Colors.NC}")
print(f"Status: {Colors.GREEN}PASS{Colors.NC}")
# ====================
# Main Logic
# ====================
def main():
"""CLI entry point"""
parser = argparse.ArgumentParser(
description='Validate format compliance for plugin and marketplace configurations',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'--file',
type=str,
required=True,
help='Path to configuration file (plugin.json or marketplace.json)'
)
parser.add_argument(
'--type',
type=str,
choices=['plugin', 'marketplace'],
required=True,
help='Configuration type'
)
parser.add_argument(
'--strict',
action='store_true',
help='Enforce HTTPS for all URLs'
)
args = parser.parse_args()
# Load configuration file
try:
with open(args.file, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print(f"{Colors.RED}❌ File not found: {args.file}{Colors.NC}", file=sys.stderr)
return 2
except json.JSONDecodeError as e:
print(f"{Colors.RED}❌ Invalid JSON: {e}{Colors.NC}", file=sys.stderr)
print(f"{Colors.BLUE} Run JSON validation first{Colors.NC}", file=sys.stderr)
return 2
# Print header
print(f"{Colors.BOLD}{Colors.BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
print(f"{Colors.BOLD}Format Validation{Colors.NC}")
print(f"{Colors.BOLD}{Colors.BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
print(f"Target: {args.file}")
print(f"Type: {args.type}")
if args.strict:
print(f"Strict HTTPS: {Colors.GREEN}Enforced{Colors.NC}")
print()
# Create validator
validator = FormatValidator(strict_https=args.strict)
# Validate based on type
if args.type == 'plugin':
result = validate_plugin_formats(data, validator)
else:
result = validate_marketplace_formats(data, validator)
# Print results
print_results(validator)
return result
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# ============================================================================
# JSON Validator Script
# ============================================================================
# Purpose: Multi-backend JSON syntax validation with detailed error reporting
# Version: 1.0.0
# Usage: ./json-validator.py --file <path> [--verbose]
# Returns: 0=valid, 1=invalid, 2=error
# Backends: jq (preferred), python3 json module (fallback)
# ============================================================================
import json
import sys
import argparse
import subprocess
import shutil
from pathlib import Path
# ====================
# Color Support
# ====================
class Colors:
"""ANSI color codes for terminal output"""
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
CYAN = '\033[0;36m'
BOLD = '\033[1m'
NC = '\033[0m'
@classmethod
def disable(cls):
"""Disable colors for non-TTY output"""
cls.RED = cls.GREEN = cls.YELLOW = cls.BLUE = cls.CYAN = cls.BOLD = cls.NC = ''
if not sys.stdout.isatty():
Colors.disable()
# ====================
# Backend Detection
# ====================
def detect_backend():
"""Detect available JSON validation backend"""
if shutil.which('jq'):
return 'jq'
elif sys.version_info >= (3, 0):
return 'python3'
else:
return 'none'
def print_backend_info():
"""Print detected backend information"""
backend = detect_backend()
if backend == 'jq':
print(f"{Colors.GREEN}✅ Backend: jq (preferred){Colors.NC}")
elif backend == 'python3':
print(f"{Colors.YELLOW}⚠️ Backend: python3 (fallback){Colors.NC}")
else:
print(f"{Colors.RED}❌ No JSON validator available{Colors.NC}")
print(f"{Colors.BLUE} Install jq for better error messages: apt-get install jq{Colors.NC}")
return backend
# ====================
# JQ Backend
# ====================
def validate_with_jq(file_path, verbose=False):
"""Validate JSON using jq (provides better error messages)"""
try:
result = subprocess.run(
['jq', 'empty', file_path],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
print(f"{Colors.GREEN}✅ Valid JSON: {file_path}{Colors.NC}")
print(f"Backend: jq")
return 0
else:
# Parse jq error message
error_msg = result.stderr.strip()
print(f"{Colors.RED}❌ Invalid JSON: {file_path}{Colors.NC}")
if verbose:
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
print(f"{Colors.RED}Error Details:{Colors.NC}")
print(f" {error_msg}")
print()
print(f"{Colors.YELLOW}Remediation:{Colors.NC}")
print(" - Check for missing commas between object properties")
print(" - Verify bracket matching: [ ] { }")
print(" - Ensure proper string quoting")
print(" - Use a JSON formatter/linter in your editor")
else:
# Extract line number if available
if "parse error" in error_msg.lower():
print(f"Error: {error_msg}")
return 1
except FileNotFoundError:
print(f"{Colors.RED}❌ File not found: {file_path}{Colors.NC}", file=sys.stderr)
return 2
except Exception as e:
print(f"{Colors.RED}❌ Error running jq: {e}{Colors.NC}", file=sys.stderr)
return 2
# ====================
# Python3 Backend
# ====================
def validate_with_python(file_path, verbose=False):
"""Validate JSON using Python's json module (universal fallback)"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Attempt to parse JSON
json.loads(content)
print(f"{Colors.GREEN}✅ Valid JSON: {file_path}{Colors.NC}")
print(f"Backend: python3")
return 0
except FileNotFoundError:
print(f"{Colors.RED}❌ File not found: {file_path}{Colors.NC}", file=sys.stderr)
return 2
except json.JSONDecodeError as e:
print(f"{Colors.RED}❌ Invalid JSON: {file_path}{Colors.NC}")
if verbose:
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
print(f"{Colors.RED}Error Details:{Colors.NC}")
print(f" Line: {e.lineno}")
print(f" Column: {e.colno}")
print(f" Issue: {e.msg}")
print()
# Show problematic section
try:
lines = content.split('\n')
start = max(0, e.lineno - 3)
end = min(len(lines), e.lineno + 2)
print(f"Problematic Section (lines {start+1}-{end}):")
for i in range(start, end):
line_num = i + 1
marker = "" if line_num == e.lineno else " "
print(f" {marker} {line_num:3d} | {lines[i]}")
print()
except:
pass
print(f"{Colors.YELLOW}Remediation:{Colors.NC}")
print(" - Check for missing commas between array elements or object properties")
print(" - Verify bracket matching: [ ] { }")
print(" - Ensure all strings are properly quoted")
print(" - Use a JSON formatter/linter in your editor")
else:
print(f"Error: {e.msg} at line {e.lineno}, column {e.colno}")
return 1
except Exception as e:
print(f"{Colors.RED}❌ Error reading file: {e}{Colors.NC}", file=sys.stderr)
return 2
# ====================
# Main Validation
# ====================
def validate_json(file_path, verbose=False):
"""Main validation function with backend selection"""
backend = detect_backend()
if backend == 'none':
print(f"{Colors.RED}❌ No JSON validation backend available{Colors.NC}", file=sys.stderr)
print(f"{Colors.BLUE} Install jq or ensure python3 is available{Colors.NC}", file=sys.stderr)
return 2
if backend == 'jq':
return validate_with_jq(file_path, verbose)
else:
return validate_with_python(file_path, verbose)
# ====================
# CLI Interface
# ====================
def main():
"""CLI entry point"""
parser = argparse.ArgumentParser(
description='Validate JSON file syntax with multi-backend support',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
./json-validator.py --file plugin.json
./json-validator.py --file marketplace.json --verbose
./json-validator.py --detect
Backends:
- jq (preferred): Fast, excellent error messages
- python3 (fallback): Universal availability
Exit codes:
0: Valid JSON
1: Invalid JSON
2: File error or backend unavailable
'''
)
parser.add_argument(
'--file',
type=str,
help='Path to JSON file to validate'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Show detailed error information and remediation'
)
parser.add_argument(
'--detect',
action='store_true',
help='Detect and display available backend'
)
args = parser.parse_args()
# Handle backend detection
if args.detect:
print_backend_info()
return 0
# Validate required arguments
if not args.file:
parser.print_help()
return 2
# Perform validation
return validate_json(args.file, args.verbose)
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,265 @@
#!/usr/bin/env bash
# ============================================================================
# Schema Differ Script
# ============================================================================
# Purpose: Compare configuration against reference schemas and validate plugin entries
# Version: 1.0.0
# Usage: ./schema-differ.sh <marketplace-file> [index]
# Returns: 0=all valid, 1=validation errors, 2=error
# ============================================================================
set -euo pipefail
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../../scripts/validate-lib.sh"
# ====================
# Configuration
# ====================
readonly MARKETPLACE_FILE="${1:-}"
readonly INDEX="${2:-all}"
# ====================
# Plugin Entry Validation
# ====================
validate_plugin_entry() {
local index=$1
local entry_json=$2
local strict=${3:-false}
local has_errors=0
local has_warnings=0
# Extract fields using json_get would be complex here, so use jq/python inline
local name version source description author keywords license
name=$(echo "${entry_json}" | jq -r '.name // empty' 2>/dev/null || echo "")
version=$(echo "${entry_json}" | jq -r '.version // empty' 2>/dev/null || echo "")
source=$(echo "${entry_json}" | jq -r '.source // empty' 2>/dev/null || echo "")
description=$(echo "${entry_json}" | jq -r '.description // empty' 2>/dev/null || echo "")
author=$(echo "${entry_json}" | jq -r '.author // empty' 2>/dev/null || echo "")
keywords=$(echo "${entry_json}" | jq -r '.keywords // empty' 2>/dev/null || echo "")
license=$(echo "${entry_json}" | jq -r '.license // empty' 2>/dev/null || echo "")
echo ""
print_section "Entry ${index}: ${name:-<unnamed>}"
# Required fields
echo " Required (3):"
# name (required, lowercase-hyphen)
if [[ -z "${name}" ]]; then
print_error " name: Missing (REQUIRED)"
((has_errors++))
elif ! validate_name_format "${name}"; then
print_error " name: \"${name}\" - Invalid format"
print_info " Expected: lowercase-hyphen (my-plugin)"
((has_errors++))
else
print_success " name: \"${name}\""
fi
# source (required, valid format)
if [[ -z "${source}" ]]; then
print_error " source: Missing (REQUIRED)"
((has_errors++))
elif ! validate_source_format "${source}"; then
print_error " source: \"${source}\" - Invalid format"
print_info " Valid: ./path, github:user/repo, https://url"
((has_errors++))
else
print_success " source: \"${source}\""
fi
# description (required, non-empty)
if [[ -z "${description}" ]]; then
print_error " description: Missing (REQUIRED)"
((has_errors++))
else
# Truncate for display
local desc_display="${description}"
if [[ ${#description} -gt 50 ]]; then
desc_display="${description:0:47}..."
fi
print_success " description: \"${desc_display}\""
fi
echo ""
echo " Recommended (4):"
# version (recommended, semver)
if [[ -z "${version}" ]]; then
print_warning " version: Missing"
((has_warnings++))
elif ! validate_semver "${version}"; then
print_warning " version: \"${version}\" - Invalid semver"
((has_warnings++))
else
print_success " version: \"${version}\""
fi
# author (recommended)
if [[ -z "${author}" || "${author}" == "null" ]]; then
print_warning " author: Missing"
((has_warnings++))
else
print_success " author: Present"
fi
# keywords (recommended)
if [[ -z "${keywords}" || "${keywords}" == "null" || "${keywords}" == "[]" ]]; then
print_warning " keywords: Missing"
((has_warnings++))
else
local keyword_count
keyword_count=$(echo "${entry_json}" | jq '.keywords | length' 2>/dev/null || echo "0")
print_success " keywords: ${keyword_count} items"
fi
# license (recommended, SPDX)
if [[ -z "${license}" ]]; then
print_warning " license: Missing"
((has_warnings++))
elif ! validate_license "${license}"; then
print_warning " license: \"${license}\" - Unknown SPDX identifier"
((has_warnings++))
else
print_success " license: \"${license}\""
fi
# Entry status
echo ""
if [[ ${has_errors} -eq 0 ]]; then
if [[ ${has_warnings} -eq 0 ]]; then
print_success "Status: PASS (no issues)"
else
print_info "Status: PASS with ${has_warnings} warning(s)"
fi
return 0
else
print_error "Status: FAIL (${has_errors} critical issues, ${has_warnings} warnings)"
return 1
fi
}
# ====================
# Main Logic
# ====================
main() {
# Validate arguments
if [[ -z "${MARKETPLACE_FILE}" ]]; then
print_error "Usage: $0 <marketplace-file> [index]"
exit 2
fi
if [[ ! -f "${MARKETPLACE_FILE}" ]]; then
print_error "Marketplace file not found: ${MARKETPLACE_FILE}"
exit 2
fi
# Validate JSON syntax
if ! validate_json_syntax "${MARKETPLACE_FILE}"; then
print_error "Invalid JSON in ${MARKETPLACE_FILE}"
print_info "Run JSON validation first to fix syntax errors"
exit 2
fi
# Print header
print_header "Plugin Entries Validation"
echo "Marketplace: ${MARKETPLACE_FILE}"
echo ""
# Get plugins array length
local plugin_count
plugin_count=$(get_json_array_length "${MARKETPLACE_FILE}" ".plugins")
if [[ ${plugin_count} -eq 0 ]]; then
print_warning "No plugin entries found in marketplace"
print_info "The plugins array is empty"
exit 0
fi
echo "Total Entries: ${plugin_count}"
# Determine which entries to validate
local entries_to_check=()
if [[ "${INDEX}" == "all" ]]; then
for ((i=0; i<plugin_count; i++)); do
entries_to_check+=("$i")
done
elif [[ "${INDEX}" =~ ^[0-9]+$ ]]; then
if [[ ${INDEX} -ge ${plugin_count} ]]; then
print_error "Invalid index: ${INDEX} (valid range: 0-$((plugin_count-1)))"
exit 2
fi
entries_to_check=("${INDEX}")
else
print_error "Invalid index: ${INDEX} (must be number or 'all')"
exit 2
fi
# Validate each entry
local failed_count=0
local total_errors=0
local total_warnings=0
for idx in "${entries_to_check[@]}"; do
# Extract plugin entry
local entry_json
if command -v jq &> /dev/null; then
entry_json=$(jq ".plugins[${idx}]" "${MARKETPLACE_FILE}" 2>/dev/null)
else
# Fallback to python
entry_json=$(python3 <<EOF 2>/dev/null
import json
with open('${MARKETPLACE_FILE}') as f:
data = json.load(f)
print(json.dumps(data['plugins'][${idx}]))
EOF
)
fi
# Validate entry
if ! validate_plugin_entry "${idx}" "${entry_json}" "false"; then
((failed_count++))
fi
done
# Summary
echo ""
print_header "Summary"
local passed_count=$((${#entries_to_check[@]} - failed_count))
local pass_percentage=$((passed_count * 100 / ${#entries_to_check[@]}))
echo "Total Entries: ${#entries_to_check[@]}"
if [[ ${passed_count} -gt 0 ]]; then
print_success "Passed: ${passed_count} (${pass_percentage}%)"
fi
if [[ ${failed_count} -gt 0 ]]; then
print_error "Failed: ${failed_count} ($((100 - pass_percentage))%)"
fi
echo ""
if [[ ${failed_count} -eq 0 ]]; then
print_header "✅ PASS: All plugin entries valid"
exit 0
else
print_header "❌ FAIL: ${failed_count} plugin entries have errors"
echo ""
print_info "Action Required:"
echo " Fix validation errors in plugin entries"
echo " Ensure all required fields are present"
echo " Use correct formats for names, versions, and sources"
exit 1
fi
}
main "$@"