fix: Address invalid JSON field in installation suite

- Separate SuperClaude metadata from Claude Code settings.json
- Create .superclaude-metadata.json for framework-specific data
- Fix JSON validation issues with settings management
- Update all components to use proper metadata storage
- Maintain compatibility with Claude Code settings format
- Add migration support for existing installations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
NomenAK 2025-07-14 16:34:20 +02:00
parent 6425be82eb
commit 625088df64
8 changed files with 229 additions and 76 deletions

View File

@ -217,7 +217,7 @@ class Installer:
if success: if success:
self.installed_components.add(component_name) self.installed_components.add(component_name)
self._update_settings_registry(component) # Component handles its own metadata registration
else: else:
self.failed_components.add(component_name) self.failed_components.add(component_name)
@ -296,8 +296,7 @@ class Installer:
return True return True
else: else:
success = component.uninstall() success = component.uninstall()
if success: # Component handles its own metadata removal
self._remove_from_settings_registry(component_name)
return success return success
except Exception as e: except Exception as e:
print(f"Error uninstalling {component_name}: {e}") print(f"Error uninstalling {component_name}: {e}")

View File

@ -97,8 +97,8 @@ class CommandsComponent(Component):
return files return files
def get_settings_modifications(self) -> Dict[str, Any]: def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get settings modifications""" """Get metadata modifications for commands component"""
return { return {
"components": { "components": {
"commands": { "commands": {
@ -109,6 +109,11 @@ class CommandsComponent(Component):
} }
} }
def get_settings_modifications(self) -> Dict[str, Any]:
"""Get settings.json modifications (now only Claude Code compatible settings)"""
# Return empty dict as we don't modify Claude Code settings
return {}
def install(self, config: Dict[str, Any]) -> bool: def install(self, config: Dict[str, Any]) -> bool:
"""Install commands component""" """Install commands component"""
try: try:
@ -155,13 +160,17 @@ class CommandsComponent(Component):
self.logger.error(f"Only {success_count}/{len(files_to_install)} command files copied successfully") self.logger.error(f"Only {success_count}/{len(files_to_install)} command files copied successfully")
return False return False
# Update settings.json # Update metadata
try: try:
settings_mods = self.get_settings_modifications() # Add component registration to metadata
self.settings_manager.update_settings(settings_mods) self.settings_manager.add_component_registration("commands", {
self.logger.info("Updated settings.json with commands component registration") "version": "3.0.0",
"category": "commands",
"files_count": len(self.command_files)
})
self.logger.info("Updated metadata with commands component registration")
except Exception as e: except Exception as e:
self.logger.error(f"Failed to update settings.json: {e}") self.logger.error(f"Failed to update metadata: {e}")
return False return False
self.logger.success(f"Commands component installed successfully ({success_count} command files)") self.logger.success(f"Commands component installed successfully ({success_count} command files)")
@ -198,13 +207,13 @@ class CommandsComponent(Component):
except Exception as e: except Exception as e:
self.logger.warning(f"Could not remove commands directory: {e}") self.logger.warning(f"Could not remove commands directory: {e}")
# Update settings.json to remove commands component # Update metadata to remove commands component
try: try:
if self.settings_manager.is_component_installed("commands"): if self.settings_manager.is_component_installed("commands"):
self.settings_manager.remove_component_registration("commands") self.settings_manager.remove_component_registration("commands")
self.logger.info("Removed commands component from settings.json") self.logger.info("Removed commands component from metadata")
except Exception as e: except Exception as e:
self.logger.warning(f"Could not update settings.json: {e}") self.logger.warning(f"Could not update metadata: {e}")
self.logger.success(f"Commands component uninstalled ({removed_count} files removed)") self.logger.success(f"Commands component uninstalled ({removed_count} files removed)")
return True return True
@ -292,9 +301,9 @@ class CommandsComponent(Component):
elif not file_path.is_file(): elif not file_path.is_file():
errors.append(f"Command file is not a regular file: {filename}") errors.append(f"Command file is not a regular file: {filename}")
# Check settings.json registration # Check metadata registration
if not self.settings_manager.is_component_installed("commands"): if not self.settings_manager.is_component_installed("commands"):
errors.append("Commands component not registered in settings.json") errors.append("Commands component not registered in metadata")
else: else:
# Check version matches # Check version matches
installed_version = self.settings_manager.get_component_version("commands") installed_version = self.settings_manager.get_component_version("commands")

View File

@ -92,8 +92,8 @@ class CoreComponent(Component):
return files return files
def get_settings_modifications(self) -> Dict[str, Any]: def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get settings.json modifications""" """Get metadata modifications for SuperClaude"""
return { return {
"framework": { "framework": {
"version": "3.0.0", "version": "3.0.0",
@ -110,6 +110,11 @@ class CoreComponent(Component):
} }
} }
def get_settings_modifications(self) -> Dict[str, Any]:
"""Get settings.json modifications (now only Claude Code compatible settings)"""
# Return empty dict as we don't modify Claude Code settings
return {}
def install(self, config: Dict[str, Any]) -> bool: def install(self, config: Dict[str, Any]) -> bool:
"""Install core component""" """Install core component"""
try: try:
@ -155,13 +160,20 @@ class CoreComponent(Component):
self.logger.error(f"Only {success_count}/{len(files_to_install)} files copied successfully") self.logger.error(f"Only {success_count}/{len(files_to_install)} files copied successfully")
return False return False
# Create or update settings.json # Create or update metadata
try: try:
settings_mods = self.get_settings_modifications() metadata_mods = self.get_metadata_modifications()
self.settings_manager.update_settings(settings_mods) # Update metadata directly
self.logger.info("Updated settings.json with framework configuration") existing_metadata = self.settings_manager.load_metadata()
merged_metadata = self.settings_manager._deep_merge(existing_metadata, metadata_mods)
self.settings_manager.save_metadata(merged_metadata)
self.logger.info("Updated metadata with framework configuration")
# Migrate any existing SuperClaude data from settings.json
if self.settings_manager.migrate_superclaude_data():
self.logger.info("Migrated existing SuperClaude data from settings.json")
except Exception as e: except Exception as e:
self.logger.error(f"Failed to update settings.json: {e}") self.logger.error(f"Failed to update metadata: {e}")
return False return False
# Create additional directories for other components # Create additional directories for other components
@ -193,13 +205,13 @@ class CoreComponent(Component):
else: else:
self.logger.warning(f"Could not remove {filename}") self.logger.warning(f"Could not remove {filename}")
# Update settings.json to remove core component # Update metadata to remove core component
try: try:
if self.settings_manager.is_component_installed("core"): if self.settings_manager.is_component_installed("core"):
self.settings_manager.remove_component_registration("core") self.settings_manager.remove_component_registration("core")
self.logger.info("Removed core component from settings.json") self.logger.info("Removed core component from metadata")
except Exception as e: except Exception as e:
self.logger.warning(f"Could not update settings.json: {e}") self.logger.warning(f"Could not update metadata: {e}")
self.logger.success(f"Core component uninstalled ({removed_count} files removed)") self.logger.success(f"Core component uninstalled ({removed_count} files removed)")
return True return True
@ -278,9 +290,9 @@ class CoreComponent(Component):
elif not file_path.is_file(): elif not file_path.is_file():
errors.append(f"Framework file is not a regular file: {filename}") errors.append(f"Framework file is not a regular file: {filename}")
# Check settings.json registration # Check metadata registration
if not self.settings_manager.is_component_installed("core"): if not self.settings_manager.is_component_installed("core"):
errors.append("Core component not registered in settings.json") errors.append("Core component not registered in metadata")
else: else:
# Check version matches # Check version matches
installed_version = self.settings_manager.get_component_version("core") installed_version = self.settings_manager.get_component_version("core")
@ -288,18 +300,18 @@ class CoreComponent(Component):
if installed_version != expected_version: if installed_version != expected_version:
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}") errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
# Check settings.json structure # Check metadata structure
try: try:
framework_config = self.settings_manager.get_setting("framework") framework_config = self.settings_manager.get_metadata_setting("framework")
if not framework_config: if not framework_config:
errors.append("Missing framework configuration in settings.json") errors.append("Missing framework configuration in metadata")
else: else:
required_keys = ["version", "name", "description"] required_keys = ["version", "name", "description"]
for key in required_keys: for key in required_keys:
if key not in framework_config: if key not in framework_config:
errors.append(f"Missing framework.{key} in settings.json") errors.append(f"Missing framework.{key} in metadata")
except Exception as e: except Exception as e:
errors.append(f"Could not validate settings.json: {e}") errors.append(f"Could not validate metadata: {e}")
return len(errors) == 0, errors return len(errors) == 0, errors

View File

@ -131,8 +131,8 @@ class MCPComponent(Component):
"""Get files to install (none for MCP component)""" """Get files to install (none for MCP component)"""
return [] return []
def get_settings_modifications(self) -> Dict[str, Any]: def get_metadata_modifications(self) -> Dict[str, Any]:
"""Get settings modifications""" """Get metadata modifications for MCP component"""
return { return {
"components": { "components": {
"mcp": { "mcp": {
@ -148,6 +148,11 @@ class MCPComponent(Component):
} }
} }
def get_settings_modifications(self) -> Dict[str, Any]:
"""Get settings.json modifications (now only Claude Code compatible settings)"""
# Return empty dict as we don't modify Claude Code settings
return {}
def _check_mcp_server_installed(self, server_name: str) -> bool: def _check_mcp_server_installed(self, server_name: str) -> bool:
"""Check if MCP server is already installed""" """Check if MCP server is already installed"""
try: try:
@ -292,13 +297,27 @@ class MCPComponent(Component):
self.logger.error(f"Required MCP server {server_name} failed to install") self.logger.error(f"Required MCP server {server_name} failed to install")
return False return False
# Update settings.json # Update metadata
try: try:
settings_mods = self.get_settings_modifications() # Add component registration to metadata
self.settings_manager.update_settings(settings_mods) self.settings_manager.add_component_registration("mcp", {
self.logger.info("Updated settings.json with MCP component registration") "version": "3.0.0",
"category": "integration",
"servers_count": len(self.mcp_servers)
})
# Add MCP configuration to metadata
metadata = self.settings_manager.load_metadata()
metadata["mcp"] = {
"enabled": True,
"servers": list(self.mcp_servers.keys()),
"auto_update": False
}
self.settings_manager.save_metadata(metadata)
self.logger.info("Updated metadata with MCP component registration")
except Exception as e: except Exception as e:
self.logger.error(f"Failed to update settings.json: {e}") self.logger.error(f"Failed to update metadata: {e}")
return False return False
# Verify installation # Verify installation
@ -347,13 +366,18 @@ class MCPComponent(Component):
if self._uninstall_mcp_server(server_name): if self._uninstall_mcp_server(server_name):
uninstalled_count += 1 uninstalled_count += 1
# Update settings.json to remove MCP component # Update metadata to remove MCP component
try: try:
if self.settings_manager.is_component_installed("mcp"): if self.settings_manager.is_component_installed("mcp"):
self.settings_manager.remove_component_registration("mcp") self.settings_manager.remove_component_registration("mcp")
self.logger.info("Removed MCP component from settings.json") # Also remove MCP configuration from metadata
metadata = self.settings_manager.load_metadata()
if "mcp" in metadata:
del metadata["mcp"]
self.settings_manager.save_metadata(metadata)
self.logger.info("Removed MCP component from metadata")
except Exception as e: except Exception as e:
self.logger.warning(f"Could not update settings.json: {e}") self.logger.warning(f"Could not update metadata: {e}")
self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)") self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)")
return True return True
@ -401,12 +425,18 @@ class MCPComponent(Component):
self.logger.error(f"Error updating MCP server {server_name}: {e}") self.logger.error(f"Error updating MCP server {server_name}: {e}")
failed_servers.append(server_name) failed_servers.append(server_name)
# Update settings # Update metadata
try: try:
settings_mods = self.get_settings_modifications() # Update component version in metadata
self.settings_manager.update_settings(settings_mods) metadata = self.settings_manager.load_metadata()
if "components" in metadata and "mcp" in metadata["components"]:
metadata["components"]["mcp"]["version"] = target_version
metadata["components"]["mcp"]["servers_count"] = len(self.mcp_servers)
if "mcp" in metadata:
metadata["mcp"]["servers"] = list(self.mcp_servers.keys())
self.settings_manager.save_metadata(metadata)
except Exception as e: except Exception as e:
self.logger.warning(f"Could not update settings.json: {e}") self.logger.warning(f"Could not update metadata: {e}")
if failed_servers: if failed_servers:
self.logger.warning(f"Some MCP servers failed to update: {failed_servers}") self.logger.warning(f"Some MCP servers failed to update: {failed_servers}")
@ -423,9 +453,9 @@ class MCPComponent(Component):
"""Validate MCP component installation""" """Validate MCP component installation"""
errors = [] errors = []
# Check settings.json registration # Check metadata registration
if not self.settings_manager.is_component_installed("mcp"): if not self.settings_manager.is_component_installed("mcp"):
errors.append("MCP component not registered in settings.json") errors.append("MCP component not registered in metadata")
return False, errors return False, errors
# Check version matches # Check version matches

View File

@ -23,6 +23,7 @@ class SettingsManager:
""" """
self.install_dir = install_dir self.install_dir = install_dir
self.settings_file = install_dir / "settings.json" self.settings_file = install_dir / "settings.json"
self.metadata_file = install_dir / ".superclaude-metadata.json"
self.backup_dir = install_dir / "backups" / "settings" self.backup_dir = install_dir / "backups" / "settings"
def load_settings(self) -> Dict[str, Any]: def load_settings(self) -> Dict[str, Any]:
@ -63,6 +64,77 @@ class SettingsManager:
except IOError as e: except IOError as e:
raise ValueError(f"Could not save settings to {self.settings_file}: {e}") raise ValueError(f"Could not save settings to {self.settings_file}: {e}")
def load_metadata(self) -> Dict[str, Any]:
"""
Load SuperClaude metadata from .superclaude-metadata.json
Returns:
Metadata dict (empty if file doesn't exist)
"""
if not self.metadata_file.exists():
return {}
try:
with open(self.metadata_file, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
raise ValueError(f"Could not load metadata from {self.metadata_file}: {e}")
def save_metadata(self, metadata: Dict[str, Any]) -> None:
"""
Save SuperClaude metadata to .superclaude-metadata.json
Args:
metadata: Metadata dict to save
"""
# Ensure directory exists
self.metadata_file.parent.mkdir(parents=True, exist_ok=True)
# Save with pretty formatting
try:
with open(self.metadata_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False, sort_keys=True)
except IOError as e:
raise ValueError(f"Could not save metadata to {self.metadata_file}: {e}")
def migrate_superclaude_data(self) -> bool:
"""
Migrate SuperClaude-specific data from settings.json to metadata file
Returns:
True if migration occurred, False if no data to migrate
"""
settings = self.load_settings()
# SuperClaude-specific fields to migrate
superclaude_fields = ["components", "framework", "superclaude", "mcp"]
data_to_migrate = {}
fields_found = False
# Extract SuperClaude data
for field in superclaude_fields:
if field in settings:
data_to_migrate[field] = settings[field]
fields_found = True
if not fields_found:
return False
# Load existing metadata (if any) and merge
existing_metadata = self.load_metadata()
merged_metadata = self._deep_merge(existing_metadata, data_to_migrate)
# Save to metadata file
self.save_metadata(merged_metadata)
# Remove SuperClaude fields from settings
clean_settings = {k: v for k, v in settings.items() if k not in superclaude_fields}
# Save cleaned settings
self.save_settings(clean_settings, create_backup=True)
return True
def merge_settings(self, modifications: Dict[str, Any]) -> Dict[str, Any]: def merge_settings(self, modifications: Dict[str, Any]) -> Dict[str, Any]:
""" """
Deep merge modifications into existing settings Deep merge modifications into existing settings
@ -163,25 +235,26 @@ class SettingsManager:
def add_component_registration(self, component_name: str, component_info: Dict[str, Any]) -> None: def add_component_registration(self, component_name: str, component_info: Dict[str, Any]) -> None:
""" """
Add component to registry in settings Add component to registry in metadata
Args: Args:
component_name: Name of component component_name: Name of component
component_info: Component metadata dict component_info: Component metadata dict
""" """
modification = { metadata = self.load_metadata()
"components": { if "components" not in metadata:
component_name: { metadata["components"] = {}
**component_info,
"installed_at": datetime.now().isoformat() metadata["components"][component_name] = {
} **component_info,
} "installed_at": datetime.now().isoformat()
} }
self.update_settings(modification)
self.save_metadata(metadata)
def remove_component_registration(self, component_name: str) -> bool: def remove_component_registration(self, component_name: str) -> bool:
""" """
Remove component from registry in settings Remove component from registry in metadata
Args: Args:
component_name: Name of component to remove component_name: Name of component to remove
@ -189,7 +262,12 @@ class SettingsManager:
Returns: Returns:
True if component was removed, False if not found True if component was removed, False if not found
""" """
return self.remove_setting(f"components.{component_name}") metadata = self.load_metadata()
if "components" in metadata and component_name in metadata["components"]:
del metadata["components"][component_name]
self.save_metadata(metadata)
return True
return False
def get_installed_components(self) -> Dict[str, Dict[str, Any]]: def get_installed_components(self) -> Dict[str, Dict[str, Any]]:
""" """
@ -198,7 +276,8 @@ class SettingsManager:
Returns: Returns:
Dict of component_name -> component_info Dict of component_name -> component_info
""" """
return self.get_setting("components", {}) metadata = self.load_metadata()
return metadata.get("components", {})
def is_component_installed(self, component_name: str) -> bool: def is_component_installed(self, component_name: str) -> bool:
""" """
@ -229,27 +308,51 @@ class SettingsManager:
def update_framework_version(self, version: str) -> None: def update_framework_version(self, version: str) -> None:
""" """
Update SuperClaude framework version in settings Update SuperClaude framework version in metadata
Args: Args:
version: Framework version string version: Framework version string
""" """
modification = { metadata = self.load_metadata()
"framework": { if "framework" not in metadata:
"version": version, metadata["framework"] = {}
"updated_at": datetime.now().isoformat()
} metadata["framework"]["version"] = version
} metadata["framework"]["updated_at"] = datetime.now().isoformat()
self.update_settings(modification)
self.save_metadata(metadata)
def get_framework_version(self) -> Optional[str]: def get_framework_version(self) -> Optional[str]:
""" """
Get SuperClaude framework version from settings Get SuperClaude framework version from metadata
Returns: Returns:
Version string or None if not set Version string or None if not set
""" """
return self.get_setting("framework.version") metadata = self.load_metadata()
framework = metadata.get("framework", {})
return framework.get("version")
def get_metadata_setting(self, key_path: str, default: Any = None) -> Any:
"""
Get metadata value using dot-notation path
Args:
key_path: Dot-separated path (e.g., "framework.version")
default: Default value if key not found
Returns:
Metadata value or default
"""
metadata = self.load_metadata()
try:
value = metadata
for key in key_path.split('.'):
value = value[key]
return value
except (KeyError, TypeError):
return default
def _deep_merge(self, base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]: def _deep_merge(self, base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
""" """

View File

@ -241,9 +241,9 @@ def create_backup_metadata(install_dir: Path) -> Dict[str, Any]:
} }
try: try:
# Get installed components # Get installed components from metadata
settings_manager = SettingsManager(install_dir) settings_manager = SettingsManager(install_dir)
framework_config = settings_manager.get_setting("framework") framework_config = settings_manager.get_metadata_setting("framework")
if framework_config: if framework_config:
metadata["framework_version"] = framework_config.get("version", "unknown") metadata["framework_version"] = framework_config.get("version", "unknown")

View File

@ -102,8 +102,8 @@ def get_installed_components(install_dir: Path) -> Dict[str, str]:
settings_manager = SettingsManager(install_dir) settings_manager = SettingsManager(install_dir)
components = {} components = {}
# Check for framework configuration # Check for framework configuration in metadata
framework_config = settings_manager.get_setting("framework") framework_config = settings_manager.get_metadata_setting("framework")
if framework_config and "components" in framework_config: if framework_config and "components" in framework_config:
for component_name in framework_config["components"]: for component_name in framework_config["components"]:
version = settings_manager.get_component_version(component_name) version = settings_manager.get_component_version(component_name)

View File

@ -98,8 +98,8 @@ def get_installed_components(install_dir: Path) -> Dict[str, str]:
settings_manager = SettingsManager(install_dir) settings_manager = SettingsManager(install_dir)
components = {} components = {}
# Check for framework configuration # Check for framework configuration in metadata
framework_config = settings_manager.get_setting("framework") framework_config = settings_manager.get_metadata_setting("framework")
if framework_config and "components" in framework_config: if framework_config and "components" in framework_config:
for component_name in framework_config["components"]: for component_name in framework_config["components"]:
version = settings_manager.get_component_version(component_name) version = settings_manager.get_component_version(component_name)