TradingAgents/.claude/lib/update_plugin.py

462 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Update Plugin CLI - Interactive command-line interface for plugin updates
This module provides CLI for plugin updates with:
- Interactive confirmation prompts
- Check-only mode (dry-run)
- Non-interactive mode (--yes flag)
- JSON output for scripting
- Verbose logging
- Exit codes: 0=success, 1=error, 2=no update needed
Features:
- Parse CLI arguments (--check-only, --yes, --auto-backup, --verbose, --json)
- Display version comparison (project vs marketplace)
- Interactive confirmation prompts
- Display update summary
- Handle user consent (yes/no/cancel)
Usage:
# Interactive update
python update_plugin.py
# Check for updates only
python update_plugin.py --check-only
# Non-interactive update
python update_plugin.py --yes
# JSON output for scripting
python update_plugin.py --json
Exit Codes:
0: Success (update performed or already up-to-date)
1: Error (update failed)
2: No update needed (when --check-only)
Date: 2025-11-09
Issue: GitHub #50 Phase 2 - Interactive /update-plugin command
Agent: implementer
See error-handling-patterns skill for exception hierarchy and error handling best practices.
Design Patterns:
See library-design-patterns skill for standardized design patterns.
"""
import argparse
import json
import sys
from pathlib import Path
# Import with fallback for both dev (plugins/) and installed (.claude/lib/) environments
try:
# Development environment
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from plugins.autonomous_dev.lib.plugin_updater import (
PluginUpdater,
UpdateResult,
UpdateError,
)
from plugins.autonomous_dev.lib.version_detector import VersionComparison
from plugins.autonomous_dev.lib.hook_activator import HookActivator
except ImportError:
# Installed environment (.claude/lib/)
from plugin_updater import (
PluginUpdater,
UpdateResult,
UpdateError,
)
from version_detector import VersionComparison
from hook_activator import HookActivator
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
argparse.Namespace with parsed arguments
Arguments:
--check-only: Check for updates without performing update
--yes: Skip confirmation prompts (non-interactive mode)
--auto-backup: Create backup before update (default: True)
--no-backup: Skip backup creation (advanced users only)
--verbose: Enable verbose logging
--json: Output JSON for scripting
--project-root: Path to project root (default: current directory)
--plugin-name: Name of plugin to update (default: autonomous-dev)
"""
parser = argparse.ArgumentParser(
description="Update Claude Code plugin with version detection and backup",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Interactive update
python update_plugin.py
# Check for updates only
python update_plugin.py --check-only
# Non-interactive update
python update_plugin.py --yes
# Update without backup (advanced)
python update_plugin.py --yes --no-backup
# JSON output for scripting
python update_plugin.py --json
Exit Codes:
0: Success (update performed or already up-to-date)
1: Error (update failed)
2: No update needed (when --check-only)
""",
)
parser.add_argument(
"--check-only",
action="store_true",
help="Check for updates without performing update (dry-run mode)",
)
parser.add_argument(
"--yes",
"-y",
action="store_true",
help="Skip confirmation prompts (non-interactive mode)",
)
parser.add_argument(
"--auto-backup",
action="store_true",
default=True,
help="Create backup before update (default: enabled)",
)
parser.add_argument(
"--no-backup",
action="store_true",
help="Skip backup creation (advanced users only, overrides --auto-backup)",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose logging",
)
parser.add_argument(
"--json",
action="store_true",
help="Output JSON for scripting (machine-readable)",
)
parser.add_argument(
"--project-root",
type=str,
default=None,
help="Path to project root directory (default: current directory)",
)
parser.add_argument(
"--plugin-name",
type=str,
default="autonomous-dev",
help="Name of plugin to update (default: autonomous-dev)",
)
parser.add_argument(
"--activate-hooks",
action="store_true",
default=None,
help="Automatically activate hooks after update",
)
parser.add_argument(
"--no-activate-hooks",
dest="activate_hooks",
action="store_false",
help="Skip hook activation after update",
)
args = parser.parse_args()
# Handle --no-backup override
if args.no_backup:
args.auto_backup = False
return args
def confirm_update(version_comparison: VersionComparison) -> bool:
"""Interactive confirmation prompt for update.
Args:
version_comparison: VersionComparison object with version info
Returns:
True if user confirms, False otherwise
"""
# Display version comparison
print("\n" + "=" * 60)
print("Plugin Update Available")
print("=" * 60)
print(f"Current version: {version_comparison.project_version}")
print(f"New version: {version_comparison.marketplace_version}")
print(f"Status: {version_comparison.status.replace('_', ' ').title()}")
print("=" * 60)
# Prompt for confirmation
while True:
response = input("\nDo you want to proceed with the update? [y/N]: ").strip().lower()
if response in ("y", "yes"):
return True
elif response in ("n", "no", ""):
return False
else:
print("Invalid response. Please enter 'y' or 'n'.")
def prompt_for_hook_activation(is_first_install: bool) -> bool:
"""Prompt user for hook activation.
Args:
is_first_install: Whether this is a first install
Returns:
True if user confirms (or first install), False otherwise
"""
# Auto-activate on first install
if is_first_install:
return True
# Interactive prompt for updates
print("\n" + "=" * 60)
print("Hook Activation")
print("=" * 60)
print("Activate automatic hooks? This will configure:")
print(" - Auto-format on save (black + isort)")
print(" - Auto-test before push")
print(" - Auto-update project progress")
print(" - Display project context on prompts")
print("=" * 60)
while True:
response = input("\nActivate hooks? [Y/n]: ").strip().lower()
if response in ("", "y", "yes"):
return True
elif response in ("n", "no"):
return False
else:
print("Invalid response. Please enter 'y' or 'n'.")
def display_version_comparison(
version_comparison: VersionComparison,
verbose: bool = False,
) -> None:
"""Display version comparison in human-readable format.
Args:
version_comparison: VersionComparison object
verbose: Whether to show verbose details
"""
print("\n" + "=" * 60)
print("Version Check")
print("=" * 60)
print(f"Project version: {version_comparison.project_version or 'N/A'}")
print(f"Marketplace version: {version_comparison.marketplace_version or 'N/A'}")
print(f"Status: {version_comparison.status.replace('_', ' ').title()}")
if verbose:
print(f"Is upgrade: {version_comparison.is_upgrade}")
print(f"Is downgrade: {version_comparison.is_downgrade}")
if version_comparison.message:
print(f"Message: {version_comparison.message}")
print("=" * 60 + "\n")
def display_update_summary(
result: UpdateResult,
json_output: bool = False,
) -> None:
"""Display update result summary.
Args:
result: UpdateResult object
json_output: Whether to output JSON format
"""
if json_output:
# JSON output for scripting
output = {
"success": result.success,
"updated": result.updated,
"message": result.message,
"old_version": result.old_version,
"new_version": result.new_version,
"backup_path": str(result.backup_path) if result.backup_path else None,
"rollback_performed": result.rollback_performed,
"hooks_activated": result.hooks_activated,
"details": result.details,
}
print(json.dumps(output, indent=2))
else:
# Human-readable output
print("\n" + "=" * 60)
print("Update Result")
print("=" * 60)
print(result.summary)
print("=" * 60 + "\n")
def main() -> int:
"""Main CLI entry point.
Returns:
Exit code: 0=success, 1=error, 2=no update needed
"""
try:
# Parse arguments
args = parse_args()
# Determine project root
project_root = Path(args.project_root) if args.project_root else Path.cwd()
# Initialize updater
try:
updater = PluginUpdater(
project_root=project_root,
plugin_name=args.plugin_name,
)
except UpdateError as e:
if args.json:
print(json.dumps({"success": False, "error": str(e)}, indent=2))
else:
print(f"Error: {e}", file=sys.stderr)
return 1
# Check for updates
try:
version_comparison = updater.check_for_updates()
except UpdateError as e:
if args.json:
print(json.dumps({"success": False, "error": str(e)}, indent=2))
else:
print(f"Error checking for updates: {e}", file=sys.stderr)
return 1
# Check-only mode
if args.check_only:
if not args.json:
display_version_comparison(version_comparison, verbose=args.verbose)
if version_comparison.status == VersionComparison.UP_TO_DATE:
print("Plugin is already up to date.")
return 0
elif version_comparison.is_upgrade:
print("Update available.")
return 2
elif version_comparison.is_downgrade:
print("Downgrade would occur (not recommended).")
return 2
else:
print("Status: " + version_comparison.status)
return 2
else:
# JSON output for check-only
output = {
"project_version": version_comparison.project_version,
"marketplace_version": version_comparison.marketplace_version,
"status": version_comparison.status,
"is_upgrade": version_comparison.is_upgrade,
"is_downgrade": version_comparison.is_downgrade,
"message": version_comparison.message,
}
print(json.dumps(output, indent=2))
if version_comparison.status == VersionComparison.UP_TO_DATE:
return 0
else:
return 2
# Already up-to-date
if version_comparison.status == VersionComparison.UP_TO_DATE:
if not args.json:
print("Plugin is already up to date.")
else:
print(json.dumps({
"success": True,
"updated": False,
"message": "Plugin is already up to date",
"version": version_comparison.project_version,
}, indent=2))
return 0
# Interactive confirmation (unless --yes)
if not args.yes and not args.json:
if not confirm_update(version_comparison):
print("Update cancelled by user.")
return 0
# Determine hook activation preference
if args.activate_hooks is not None:
# Explicit flag provided
activate_hooks = args.activate_hooks
elif args.yes or args.json:
# Non-interactive mode: activate by default
activate_hooks = True
else:
# Interactive mode: prompt user
activator = HookActivator(project_root=project_root)
is_first_install = activator.is_first_install()
activate_hooks = prompt_for_hook_activation(is_first_install)
# Perform update
if args.verbose and not args.json:
print(f"\nUpdating {args.plugin_name}...")
if args.auto_backup:
print("Creating backup...")
if activate_hooks:
print("Hook activation enabled...")
try:
result = updater.update(
auto_backup=args.auto_backup,
skip_confirm=args.yes,
activate_hooks=activate_hooks,
)
except UpdateError as e:
if args.json:
print(json.dumps({"success": False, "error": str(e)}, indent=2))
else:
print(f"Update failed: {e}", file=sys.stderr)
return 1
# Display result
display_update_summary(result, json_output=args.json)
# Return exit code
if result.success:
return 0
else:
return 1
except KeyboardInterrupt:
print("\nUpdate cancelled by user.", file=sys.stderr)
return 1
except Exception as e:
if args.json if 'args' in locals() else False:
print(json.dumps({"success": False, "error": str(e)}, indent=2))
else:
print(f"Unexpected error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())