Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:51:59 +08:00
commit 38e80921c8
89 changed files with 20480 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Navigator Plugin Updater
Executes plugin update with retry logic and verification.
Usage:
python plugin_updater.py [--target-version VERSION]
"""
import argparse
import json
import subprocess
import sys
import time
from typing import Dict
def update_plugin_via_claude() -> Dict:
"""
Execute /plugin update navigator command.
Returns:
Dict with success status and output
"""
try:
# Execute update command
result = subprocess.run(
['claude', 'plugin', 'update', 'navigator'],
capture_output=True,
text=True,
timeout=60
)
success = result.returncode == 0
return {
'success': success,
'output': result.stdout,
'error': result.stderr,
'method': 'update'
}
except subprocess.TimeoutExpired:
return {
'success': False,
'error': 'Update timed out after 60 seconds',
'method': 'update'
}
except FileNotFoundError:
return {
'success': False,
'error': 'claude command not found. Is Claude Code installed?',
'method': 'update'
}
except Exception as e:
return {
'success': False,
'error': str(e),
'method': 'update'
}
def reinstall_plugin() -> Dict:
"""
Uninstall and reinstall Navigator plugin.
Returns:
Dict with success status
"""
try:
# Uninstall
uninstall_result = subprocess.run(
['claude', 'plugin', 'uninstall', 'navigator'],
capture_output=True,
text=True,
timeout=30
)
if uninstall_result.returncode != 0:
return {
'success': False,
'error': f'Uninstall failed: {uninstall_result.stderr}',
'method': 'reinstall'
}
# Wait a moment
time.sleep(2)
# Add from marketplace
add_result = subprocess.run(
['claude', 'plugin', 'marketplace', 'add', 'alekspetrov/navigator'],
capture_output=True,
text=True,
timeout=30
)
if add_result.returncode != 0:
return {
'success': False,
'error': f'Marketplace add failed: {add_result.stderr}',
'method': 'reinstall'
}
# Wait a moment
time.sleep(2)
# Install
install_result = subprocess.run(
['claude', 'plugin', 'install', 'navigator'],
capture_output=True,
text=True,
timeout=60
)
success = install_result.returncode == 0
return {
'success': success,
'output': install_result.stdout,
'error': install_result.stderr if not success else None,
'method': 'reinstall'
}
except subprocess.TimeoutExpired:
return {
'success': False,
'error': 'Reinstall timed out',
'method': 'reinstall'
}
except Exception as e:
return {
'success': False,
'error': str(e),
'method': 'reinstall'
}
def update_with_retry(target_version: str = None) -> Dict:
"""
Update Navigator plugin with automatic retry on failure.
Args:
target_version: Optional specific version to install
Returns:
Dict with final update status
"""
report = {
'attempts': [],
'final_success': False,
'target_version': target_version
}
# Attempt 1: Normal update
print("Attempting plugin update...", file=sys.stderr)
attempt1 = update_plugin_via_claude()
report['attempts'].append(attempt1)
if attempt1['success']:
report['final_success'] = True
return report
# Attempt 2: Reinstall
print("Update failed. Attempting reinstall...", file=sys.stderr)
time.sleep(2)
attempt2 = reinstall_plugin()
report['attempts'].append(attempt2)
if attempt2['success']:
report['final_success'] = True
return report
# Both failed
return report
def get_post_update_instructions(success: bool, method: str) -> str:
"""Generate post-update instructions."""
if success:
return """
✅ Update Successful
Next steps:
1. Restart Claude Code to reload skills
2. Verify version: /plugin list
3. Update project CLAUDE.md: "Update my CLAUDE.md to latest Navigator version"
4. Try new features (if any)
"""
else:
return f"""
❌ Update Failed (method: {method})
Troubleshooting:
1. Restart Claude Code
2. Try manual update:
/plugin uninstall navigator
/plugin marketplace add alekspetrov/navigator
/plugin install navigator
3. Check internet connection
4. Report issue: https://github.com/alekspetrov/navigator/issues
"""
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description='Update Navigator plugin')
parser.add_argument('--target-version', help='Target version to install', default=None)
args = parser.parse_args()
# Run update with retry
report = update_with_retry(args.target_version)
# Add instructions
final_attempt = report['attempts'][-1] if report['attempts'] else {}
method = final_attempt.get('method', 'unknown')
report['instructions'] = get_post_update_instructions(report['final_success'], method)
# Output as JSON
print(json.dumps(report, indent=2))
# Exit code
sys.exit(0 if report['final_success'] else 1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
Navigator Plugin Verifier
Verifies that Navigator plugin update completed successfully.
Usage:
python plugin_verifier.py --expected-version 3.3.0
"""
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict, List
def get_installed_version() -> str:
"""
Get installed Navigator version from /plugin list.
Returns:
Version string or None
"""
try:
result = subprocess.run(
['claude', 'plugin', 'list'],
capture_output=True,
text=True,
timeout=10
)
for line in result.stdout.split('\n'):
if 'navigator' in line.lower():
match = re.search(r'v?(\d+\.\d+\.\d+)', line)
if match:
return match.group(1)
return None
except Exception:
return None
def find_plugin_directory() -> Path:
"""
Find Navigator plugin installation directory.
Returns:
Path to plugin directory or None
"""
possible_paths = [
Path.home() / '.config' / 'claude' / 'plugins' / 'navigator',
Path.home() / '.claude' / 'plugins' / 'navigator',
Path.home() / 'Library' / 'Application Support' / 'Claude' / 'plugins' / 'navigator',
]
for path in possible_paths:
if path.exists() and path.is_dir():
return path
return None
def verify_skills_exist(plugin_dir: Path, expected_skills: List[str]) -> Dict:
"""
Verify that expected skills exist in plugin directory.
Args:
plugin_dir: Path to plugin directory
expected_skills: List of skill names to check
Returns:
Dict with verification results
"""
skills_dir = plugin_dir / 'skills'
if not skills_dir.exists():
return {
'success': False,
'error': 'Skills directory not found'
}
results = {}
for skill_name in expected_skills:
skill_path = skills_dir / skill_name / 'SKILL.md'
results[skill_name] = skill_path.exists()
all_exist = all(results.values())
return {
'success': all_exist,
'skills_checked': results,
'missing_skills': [name for name, exists in results.items() if not exists]
}
def verify_plugin_json(plugin_dir: Path, expected_skills: List[str]) -> Dict:
"""
Verify that skills are registered in plugin.json.
Args:
plugin_dir: Path to plugin directory
expected_skills: List of skill names to check
Returns:
Dict with verification results
"""
plugin_json_path = plugin_dir / '.claude-plugin' / 'plugin.json'
if not plugin_json_path.exists():
return {
'success': False,
'error': 'plugin.json not found'
}
try:
with open(plugin_json_path, 'r') as f:
data = json.load(f)
registered_skills = data.get('skills', [])
# Check each expected skill
results = {}
for skill_name in expected_skills:
skill_path = f'./skills/{skill_name}'
results[skill_name] = skill_path in registered_skills
all_registered = all(results.values())
return {
'success': all_registered,
'skills_checked': results,
'unregistered_skills': [name for name, registered in results.items() if not registered]
}
except (json.JSONDecodeError, FileNotFoundError) as e:
return {
'success': False,
'error': str(e)
}
def verify_update(expected_version: str, expected_new_skills: List[str] = None) -> Dict:
"""
Comprehensive verification of Navigator plugin update.
Args:
expected_version: Expected version after update (e.g., "3.3.0")
expected_new_skills: List of new skills expected in this version
Returns:
Complete verification report
"""
report = {
'expected_version': expected_version,
'checks': {},
'overall_success': False,
'needs_restart': False
}
# Check 1: Version matches
installed_version = get_installed_version()
report['checks']['version'] = {
'expected': expected_version,
'actual': installed_version,
'success': installed_version == expected_version
}
# Check 2: Plugin directory exists
plugin_dir = find_plugin_directory()
report['checks']['plugin_directory'] = {
'success': plugin_dir is not None,
'path': str(plugin_dir) if plugin_dir else None
}
if not plugin_dir:
report['recommendation'] = 'Plugin directory not found. Reinstall Navigator.'
return report
# Check 3: New skills exist (if specified)
if expected_new_skills:
skills_check = verify_skills_exist(plugin_dir, expected_new_skills)
report['checks']['skills_exist'] = skills_check
# Check 4: Skills registered in plugin.json
registration_check = verify_plugin_json(plugin_dir, expected_new_skills)
report['checks']['skills_registered'] = registration_check
# If skills exist but verification shows they're not accessible, needs restart
if skills_check['success'] and not registration_check['success']:
report['needs_restart'] = True
# Overall success
all_checks_passed = all(
check.get('success', False)
for check in report['checks'].values()
)
report['overall_success'] = all_checks_passed
# Generate recommendation
if all_checks_passed:
report['recommendation'] = 'Update verified successfully!'
elif report['needs_restart']:
report['recommendation'] = 'Update completed. Restart Claude Code to reload skills.'
else:
failed_checks = [name for name, check in report['checks'].items() if not check.get('success')]
report['recommendation'] = f"Verification failed: {', '.join(failed_checks)}"
return report
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description='Verify Navigator plugin update')
parser.add_argument('--expected-version', required=True, help='Expected version (e.g., 3.3.0)')
parser.add_argument('--new-skills', nargs='*', help='New skills to verify', default=[])
args = parser.parse_args()
# Run verification
report = verify_update(args.expected_version, args.new_skills or None)
# Output as JSON
print(json.dumps(report, indent=2))
# Exit code
if report['overall_success']:
sys.exit(0)
elif report['needs_restart']:
sys.exit(2) # Special exit code for restart needed
else:
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""
Navigator Version Detector
Detects current Navigator version and checks for updates from GitHub releases.
Usage:
python version_detector.py
"""
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict, Optional
from urllib import request
def get_current_version() -> Optional[str]:
"""
Get currently installed Navigator version from /plugin list.
Returns:
Version string (e.g., "3.3.0") or None if not found
"""
try:
# Try to run claude plugin list command
result = subprocess.run(
['claude', 'plugin', 'list'],
capture_output=True,
text=True,
timeout=10
)
# Parse output for navigator version
for line in result.stdout.split('\n'):
if 'navigator' in line.lower():
# Extract version (e.g., "navigator (v3.3.0)" or "navigator (3.3.0)")
match = re.search(r'v?(\d+\.\d+\.\d+)', line)
if match:
return match.group(1)
return None
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
return None
def get_plugin_json_version() -> Optional[str]:
"""
Fallback: Get version from plugin.json in Navigator plugin directory.
Returns:
Version string or None
"""
# Common plugin installation paths
possible_paths = [
Path.home() / '.config' / 'claude' / 'plugins' / 'navigator' / '.claude-plugin' / 'plugin.json',
Path.home() / '.claude' / 'plugins' / 'navigator' / '.claude-plugin' / 'plugin.json',
Path.home() / 'Library' / 'Application Support' / 'Claude' / 'plugins' / 'navigator' / '.claude-plugin' / 'plugin.json',
]
for path in possible_paths:
if path.exists():
try:
with open(path, 'r') as f:
data = json.load(f)
return data.get('version')
except (json.JSONDecodeError, FileNotFoundError, PermissionError):
continue
return None
def get_latest_version_from_github() -> Dict:
"""
Get latest Navigator version from GitHub releases API.
Returns:
Dict with version, release_url, and changes
"""
try:
url = 'https://api.github.com/repos/alekspetrov/navigator/releases/latest'
req = request.Request(url)
req.add_header('User-Agent', 'Navigator-Version-Detector')
with request.urlopen(req, timeout=10) as response:
data = json.load(response)
# Extract version from tag_name (e.g., "v3.3.0" → "3.3.0")
tag_name = data.get('tag_name', '')
version = tag_name.lstrip('v')
# Parse release notes for key changes
body = data.get('body', '')
changes = parse_release_notes(body)
return {
'version': version,
'release_url': data.get('html_url', ''),
'release_date': data.get('published_at', '').split('T')[0],
'changes': changes
}
except Exception as e:
return {
'version': None,
'error': str(e)
}
def parse_release_notes(body: str) -> Dict:
"""
Parse release notes to extract key changes.
Args:
body: Release notes markdown
Returns:
Dict with new_skills, updated_skills, new_features, breaking_changes
"""
changes = {
'new_skills': [],
'updated_skills': [],
'new_features': [],
'breaking_changes': []
}
# Extract new skills
skill_pattern = r'-\s+\*\*(\w+-[\w-]+)\*\*:.*\(NEW\)'
for match in re.finditer(skill_pattern, body):
changes['new_skills'].append(match.group(1))
# Extract features from "What's New" section
features_section = re.search(r'##\s+.*What.*s New(.*?)(?=##|\Z)', body, re.DOTALL | re.IGNORECASE)
if features_section:
# Find bullet points
for line in features_section.group(1).split('\n'):
if line.strip().startswith('-') or line.strip().startswith('*'):
feature = line.strip().lstrip('-*').strip()
if feature and len(feature) < 100: # Reasonable feature description
changes['new_features'].append(feature)
# Check for breaking changes
if 'breaking change' in body.lower() or '⚠️' in body:
breaking_section = re.search(r'##\s+.*Breaking.*Changes(.*?)(?=##|\Z)', body, re.DOTALL | re.IGNORECASE)
if breaking_section:
for line in breaking_section.group(1).split('\n'):
if line.strip().startswith('-') or line.strip().startswith('*'):
change = line.strip().lstrip('-*').strip()
if change:
changes['breaking_changes'].append(change)
return changes
def compare_versions(current: str, latest: str) -> int:
"""
Compare two semantic versions.
Args:
current: Current version (e.g., "3.2.0")
latest: Latest version (e.g., "3.3.0")
Returns:
-1 if current < latest (update available)
0 if current == latest (up to date)
1 if current > latest (ahead of latest, e.g., dev version)
"""
try:
current_parts = [int(x) for x in current.split('.')]
latest_parts = [int(x) for x in latest.split('.')]
# Pad to same length
while len(current_parts) < len(latest_parts):
current_parts.append(0)
while len(latest_parts) < len(current_parts):
latest_parts.append(0)
# Compare
for c, l in zip(current_parts, latest_parts):
if c < l:
return -1
elif c > l:
return 1
return 0
except (ValueError, AttributeError):
return 0 # Can't compare, assume equal
def detect_version() -> Dict:
"""
Detect current and latest Navigator versions.
Returns:
Complete version detection report
"""
# Get current version
current_version = get_current_version()
if not current_version:
# Fallback to plugin.json
current_version = get_plugin_json_version()
# Get latest version from GitHub
latest_info = get_latest_version_from_github()
latest_version = latest_info.get('version')
# Determine if update available
update_available = False
if current_version and latest_version:
comparison = compare_versions(current_version, latest_version)
update_available = (comparison == -1)
# Build report
report = {
'current_version': current_version,
'latest_version': latest_version,
'update_available': update_available,
'release_url': latest_info.get('release_url', ''),
'release_date': latest_info.get('release_date', ''),
'changes': latest_info.get('changes', {}),
'error': latest_info.get('error'),
'recommendation': get_recommendation(current_version, latest_version, update_available)
}
return report
def get_recommendation(current: Optional[str], latest: Optional[str], update_available: bool) -> str:
"""Generate recommendation based on version status."""
if not current:
return "Navigator not detected. Install: /plugin marketplace add alekspetrov/navigator && /plugin install navigator"
if not latest:
return "Could not check for updates. Try again later or check GitHub releases manually."
if update_available:
return f"Update recommended: v{current} → v{latest}. Run: /plugin update navigator"
return f"You're on the latest version (v{current}). No update needed."
def main():
"""CLI entry point."""
report = detect_version()
# Output as JSON
print(json.dumps(report, indent=2))
# Exit with code
# 0 = up to date
# 1 = update available
# 2 = error
if report.get('error'):
sys.exit(2)
elif report.get('update_available'):
sys.exit(1)
else:
sys.exit(0)
if __name__ == '__main__':
main()