TradingAgents/.claude/scripts/configure_global_settings.py

309 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""
Configure Global Settings CLI - Fresh install permission configuration
Creates or updates ~/.claude/settings.json with correct permission patterns
for Claude Code 2.0. This script is called by install.sh during fresh install.
Features:
1. Fresh install: Create ~/.claude/settings.json from template
2. Upgrade: Preserve user customizations while fixing broken patterns
3. Broken patterns: Replace Bash(:*) with specific safe patterns
4. Non-blocking: Exit 0 even on errors (installation continues)
5. JSON output: Return structured data for install.sh consumption
6. Directory creation: Create ~/.claude/ if missing
Security:
- Path validation (CWE-22, CWE-59)
- Atomic writes with secure permissions
- Backup before modification
- No wildcards (Bash(git:*) NOT Bash(*))
Usage:
# Fresh install (no existing settings)
python3 configure_global_settings.py --template /path/to/template.json
# Upgrade (existing settings, preserve customizations)
python3 configure_global_settings.py --template /path/to/template.json --home ~/.claude
Output:
JSON to stdout: {"success": bool, "created": bool, "message": str, ...}
Exit code: Always 0 (non-blocking for install.sh)
See Also:
- plugins/autonomous-dev/lib/settings_generator.py for merge logic
- plugins/autonomous-dev/config/global_settings_template.json for template
- tests/unit/scripts/test_configure_global_settings.py for test cases
- GitHub Issue #116 for requirements
Date: 2025-12-13
Issue: GitHub #116
Agent: implementer
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, Any
# Add lib to path for imports
try:
# Try package import first
from autonomous_dev.lib.settings_generator import SettingsGenerator, SettingsGeneratorError
except ImportError:
# Fallback for direct script execution
lib_path = Path(__file__).parent.parent / "lib"
sys.path.insert(0, str(lib_path))
from settings_generator import SettingsGenerator, SettingsGeneratorError
def create_fresh_settings(template_path: Path, global_path: Path) -> Dict[str, Any]:
"""Create fresh settings.json from template.
Args:
template_path: Path to global_settings_template.json
global_path: Path to ~/.claude/settings.json
Returns:
Result dict with success status and metadata
"""
try:
# Validate template exists
if not template_path.exists():
return {
"success": False,
"created": False,
"message": f"Template file not found: {template_path}",
"error": "template_not_found"
}
# Ensure ~/.claude/ directory exists
claude_dir = global_path.parent
if not ensure_claude_directory(claude_dir):
return {
"success": False,
"created": False,
"message": f"Failed to create directory: {claude_dir}",
"error": "directory_creation_failed"
}
# Read template
template_content = template_path.read_text()
template_settings = json.loads(template_content)
# Write settings atomically
global_path.write_text(json.dumps(template_settings, indent=2) + "\n")
# Set secure permissions (owner read/write only)
global_path.chmod(0o600)
return {
"success": True,
"created": True,
"message": "Fresh install: Created settings.json from template",
"path": str(global_path),
"patterns_count": len(template_settings.get("permissions", {}).get("allow", []))
}
except PermissionError as e:
return {
"success": False,
"created": False,
"message": f"Permission denied: {e}",
"error": "permission_denied"
}
except json.JSONDecodeError as e:
return {
"success": False,
"created": False,
"message": f"Invalid JSON in template: {e}",
"error": "invalid_template_json"
}
except Exception as e:
return {
"success": False,
"created": False,
"message": f"Unexpected error: {e}",
"error": "unexpected_error"
}
def upgrade_existing_settings(global_path: Path, template_path: Path) -> Dict[str, Any]:
"""Upgrade existing settings while preserving user customizations.
Args:
global_path: Path to existing ~/.claude/settings.json
template_path: Path to global_settings_template.json
Returns:
Result dict with success status and metadata
"""
try:
# Validate inputs
if not global_path.exists():
return {
"success": False,
"created": False,
"message": f"Settings file not found: {global_path}",
"error": "settings_not_found"
}
if not template_path.exists():
return {
"success": False,
"created": False,
"message": f"Template file not found: {template_path}",
"error": "template_not_found"
}
# Use SettingsGenerator to merge settings
# Pass project_root mode to avoid requiring full plugin structure
generator = SettingsGenerator(project_root=Path.home())
# Call merge_global_settings (handles backup, merge, and write)
# Returns merged settings dict on success, raises exception on error
merged_settings = generator.merge_global_settings(
global_path,
template_path,
fix_wildcards=True,
create_backup=True
)
# Count patterns fixed (check if Bash(:*) was in original)
patterns_fixed = 0
backup_path = global_path.with_suffix(".json.backup")
try:
if backup_path.exists():
original_settings = json.loads(backup_path.read_text())
if "permissions" in original_settings and "allow" in original_settings["permissions"]:
broken = [p for p in original_settings["permissions"]["allow"] if p in ["Bash(:*)", "Bash(*)"]]
patterns_fixed = len(broken)
except:
pass
# Build message based on patterns fixed
if patterns_fixed > 0:
message = f"Settings upgraded successfully (fixed {patterns_fixed} broken patterns, preserved customizations)"
else:
message = "Settings upgraded successfully (preserved customizations)"
return {
"success": True,
"created": False,
"message": message,
"merged": True,
"patterns_fixed": patterns_fixed,
"path": str(global_path)
}
except SettingsGeneratorError as e:
return {
"success": False,
"created": False,
"message": f"Settings merge error: {e}",
"error": "merge_failed"
}
except PermissionError as e:
return {
"success": False,
"created": False,
"message": f"Permission denied: {e}",
"error": "permission_denied"
}
except Exception as e:
return {
"success": False,
"created": False,
"message": f"Unexpected error: {e}",
"error": "unexpected_error"
}
def ensure_claude_directory(claude_dir: Path) -> bool:
"""Ensure ~/.claude/ directory exists with correct permissions.
Args:
claude_dir: Path to ~/.claude/ directory
Returns:
True if directory exists/created, False on error
"""
try:
# Create directory if missing (mkdir -p behavior)
claude_dir.mkdir(parents=True, exist_ok=True)
# Set permissions (owner read/write/execute)
claude_dir.chmod(0o700)
return True
except PermissionError:
return False
except Exception:
return False
def main():
"""Main entry point for CLI."""
parser = argparse.ArgumentParser(
description="Configure ~/.claude/settings.json for autonomous-dev plugin",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Fresh install
%(prog)s --template global_settings_template.json
# Upgrade with custom home
%(prog)s --template global_settings_template.json --home ~/.claude
Exit Code:
Always exits 0 (non-blocking for install.sh)
Check JSON output "success" field for actual status
"""
)
parser.add_argument(
"--template",
type=Path,
required=True,
help="Path to global_settings_template.json"
)
parser.add_argument(
"--home",
type=Path,
default=Path.home() / ".claude",
help="Path to .claude directory (default: ~/.claude)"
)
parser.add_argument(
"--staging",
type=Path,
help="Path to staging directory (unused, for compatibility)"
)
args = parser.parse_args()
# Determine global_path
global_path = args.home / "settings.json"
# Check if settings.json already exists
if global_path.exists():
# Upgrade scenario: merge with existing settings
result = upgrade_existing_settings(global_path, args.template)
else:
# Fresh install scenario: create from template
result = create_fresh_settings(args.template, global_path)
# Output JSON to stdout
print(json.dumps(result, indent=2))
# Always exit 0 (non-blocking for install.sh)
sys.exit(0)
if __name__ == "__main__":
main()