Fixed Some Bugs (#355)

* Fix: Install only selected MCP servers and ensure valid empty backups

This commit addresses two separate issues:

1.  **MCP Installation:** The `install` command was installing all MCP servers instead of only the ones selected by the user. The `_install` method in `setup/components/mcp.py` was iterating through all available servers, not the user's selection. This has been fixed to respect the `selected_mcp_servers` configuration. A new test has been added to verify this fix.

2.  **Backup Creation:** The `create_backup` method in `setup/core/installer.py` created an invalid `.tar.gz` file when the backup source was empty. This has been fixed to ensure that a valid, empty tar archive is always created. A test was added for this as well.
Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* Fix: Correct installer validation for MCP and MCP Docs components

This commit fixes a validation issue in the installer where it would incorrectly fail after a partial installation of MCP servers.

The `MCPComponent` validation logic was checking for all "required" servers, regardless of whether they were selected by the user. This has been corrected to only validate the servers that were actually installed, by checking against the list of installed servers stored in the metadata. The metadata storage has also been fixed to only record the installed servers.

The `MCPDocsComponent` was failing validation because it was not being registered in the metadata if no documentation files were installed. This has been fixed by ensuring the post-installation hook runs even when no files are copied.

New tests have been added for both components to verify the corrected logic.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* Fix: Allow re-installation of components and correct validation logic

This commit fixes a bug that prevented new MCP servers from being installed on subsequent runs of the installer. It also fixes the validation logic that was causing failures after a partial installation.

The key changes are:
1.  A new `is_reinstallable` method has been added to the base `Component` class. This allows certain components (like the `mcp` component) to be re-run even if they are already marked as installed.
2.  The installer logic has been updated to respect this new method.
3.  The `MCPComponent` now correctly stores only the installed servers in the metadata.
4.  The validation logic for `MCPComponent` and `MCPDocsComponent` has been corrected to prevent incorrect failures.

New tests have been added to verify all aspects of the new logic.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* feat: Display authors in UI header and update author info

This commit implements the user's request to display author names and emails in the UI header of the installer.

The key changes are:
1.  The `__email__` field in `SuperClaude/__init__.py` has been updated to include both authors' emails.
2.  The `display_header` function in `setup/utils/ui.py` has been modified to read the author and email information and display it.
3.  A new test has been added to `tests/test_ui.py` to verify the new UI output.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* feat: Version bump to 4.1.0 and various fixes

This commit prepares the project for the v4.1.0 release. It includes a version bump across all relevant files and incorporates several bug fixes and feature enhancements from recent tasks.

Key changes in this release:

- **Version Bump**: The project version has been updated from 4.0.9 to 4.1.0 in all configuration files, documentation, and source code.

- **Installer Fixes**:
  - Components can now be marked as `reinstallable`, allowing them to be re-run on subsequent installations. This fixes a bug where new MCP servers could not be added.
  - The validation logic for `mcp` and `mcp_docs` components has been corrected to avoid incorrect failures.
  - A bug in the backup creation process that created invalid empty archives has been fixed.

- **UI Enhancements**:
  - Author names and emails are now displayed in the installer UI header.

- **Metadata Updates**:
  - Mithun Gowda B has been added as an author.

- **New Tests**:
  - Comprehensive tests have been added for the installer logic, MCP components, and UI changes to ensure correctness and prevent regressions.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* fix: Resolve dependencies for partial installs and other fixes

This commit addresses several issues, the main one being a dependency resolution failure during partial installations.

Key changes:
- **Dependency Resolution**: The installer now correctly resolves the full dependency tree when a user requests to install a subset of components. This fixes the "Unknown component: core" error.
- **Component Re-installation**: A new `is_reinstallable` flag allows components like `mcp` to be re-run on subsequent installs, enabling the addition of new servers.
- **Validation Logic**: The validation for `mcp` and `mcp_docs` has been corrected to avoid spurious failures.
- **UI and Metadata**: Author information has been added to the UI header and source files.
- **Version Bump**: The project version has been updated to 4.1.0.
- **Tests**: New tests have been added to cover all the above changes.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* fix: Installer fixes and version bump to 4.1.0

This commit includes a collection of fixes for the installer logic, UI enhancements, and a version bump to 4.1.0.

Key changes:
- **Dependency Resolution**: The installer now correctly resolves the full dependency tree for partial installations, fixing the "Unknown component: core" error.
- **Component Re-installation**: A new `is_reinstallable` flag allows components like `mcp` to be re-run to add new servers.
- **MCP Installation**: The non-interactive installation of the `mcp` component now correctly prompts the user to select servers.
- **Validation Logic**: The post-installation validation logic has been corrected to only validate components from the current session and to use the correct list of installed servers.
- **UI & Metadata**: Author information has been added to the UI and source files.
- **Version Bump**: The project version has been updated from 4.0.9 to 4.1.0 across all files.
- **Tests**: New tests have been added to cover all the bug fixes.

Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

* feat: Add --authors flag and multiple installer fixes

This commit introduces the `--authors` flag to display author information and includes a collection of fixes for the installer logic.

Key changes:
- **New Feature**: Added an `--authors` flag that displays the names, emails, and GitHub usernames of the project authors.
- **Dependency Resolution**: Fixed a critical bug where partial installations would fail due to unresolved dependencies.
- **Component Re-installation**: Added a mechanism to allow components to be "reinstallable", fixing an issue that prevented adding new MCP servers on subsequent runs.
- **MCP Installation**: The non-interactive installation of the `mcp` component now correctly prompts for server selection.
- **Validation Logic**: Corrected the post-installation validation to prevent spurious errors.
- **Version Bump**: The project version has been updated to 4.1.0.
- **Metadata**: Author and GitHub information has been added to the source files.
- **UI**: The installer header now displays author information.
- **Tests**: Added new tests for all new features and bug fixes.


Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>

---------
Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>
This commit is contained in:
Mithun Gowda B 2025-09-13 17:28:52 +05:30 committed by GitHub
parent caf94facc4
commit fb609c6a06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 521 additions and 86 deletions

View File

@ -6,8 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [4.0.9] - 2025-09-05
## [4.1.0] - 2025-09-13
### Added
- Display author names and emails in the installer UI header.
- `is_reinstallable` flag for components to allow re-running installation.
### Fixed
- Installer now correctly installs only selected MCP servers on subsequent runs.
- Corrected validation logic for `mcp` and `mcp_docs` components to prevent incorrect failures.
- Ensured empty backup archives are created as valid tar files.
- Addressed an issue where only selected MCPs were being installed.
- Added Mithun Gowda B as an author.
- **MCP Installer:** Addressed several critical bugs in the MCP installation and update process to improve reliability.
- Corrected the npm package name for the `morphllm` server in `setup/components/mcp.py`.
- Implemented a custom installation method for the `serena` server using `uv`, as it is not an npm package.
@ -16,8 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the `claude` CLI as a formal prerequisite for MCP server management, which was previously undocumented.
### Changed
- Version bump for PyPI release
- Updated all version references across project files
- Version bump to 4.1.0
### Technical
- Prepared package for PyPI distribution

View File

@ -518,7 +518,7 @@ This code of conduct draws inspiration from several established community standa
**Last Updated**: December 2024 (SuperClaude Framework v4.0)
**Next Review**: June 2025 (Semi-annual review cycle)
**Version**: 4.0.9 (Updated for v4 community structure and governance)
**Version**: 4.1.0 (Updated for v4 community structure and governance)
**Review Schedule:**
- **Semi-Annual Reviews**: Policy effectiveness assessment and community feedback integration

View File

@ -27,7 +27,7 @@ SuperClaude Framework transforms Claude Code into a structured development platf
**Good Bug Report Example:**
```
**Environment:**
- SuperClaude: 4.0.9
- SuperClaude: 4.1.0
- OS: Ubuntu 22.04
- Claude Code: 1.5.2
- Python: 3.9.7

View File

@ -5,7 +5,7 @@
### **Transform Claude Code with 21 Commands, 14 Agents & 6 MCP Servers**
<p align="center">
<img src="https://img.shields.io/badge/version-4.0.9-blue?style=for-the-badge" alt="Version">
<img src="https://img.shields.io/badge/version-4.1.0-blue?style=for-the-badge" alt="Version">
<img src="https://img.shields.io/badge/Python-3.8+-green?style=for-the-badge" alt="Python">
<img src="https://img.shields.io/badge/Platform-Linux%20|%20macOS%20|%20Windows-orange?style=for-the-badge" alt="Platform">
</p>
@ -266,7 +266,7 @@ SuperClaude install --dry-run
```bash
# Verify SuperClaude version
python3 -m SuperClaude --version
# Expected: SuperClaude 4.0.9
# Expected: SuperClaude 4.1.0
# List installed components
SuperClaude install --list-components

View File

@ -6,7 +6,7 @@
<p align="center">
<img src="https://img.shields.io/badge/Framework-Context_Engineering-purple?style=for-the-badge" alt="Framework">
<img src="https://img.shields.io/badge/Version-4.0.9-blue?style=for-the-badge" alt="Version">
<img src="https://img.shields.io/badge/Version-4.1.0-blue?style=for-the-badge" alt="Version">
<img src="https://img.shields.io/badge/Time_to_Start-5_Minutes-green?style=for-the-badge" alt="Quick Start">
</p>
@ -486,7 +486,7 @@ Create custom workflows
</p>
<p align="center">
<sub>SuperClaude v4.0.9 - Context Engineering for Claude Code</sub>
<sub>SuperClaude v4.1.0 - Context Engineering for Claude Code</sub>
</p>
</div>

View File

@ -13,7 +13,7 @@ Test: /sc:brainstorm "test" should ask questions
### 2. Installation Verification
```bash
python3 -m SuperClaude --version # Should show 4.0.9
python3 -m SuperClaude --version # Should show 4.1.0
# If not working:
# For pipx users
@ -71,7 +71,7 @@ pip3 install SuperClaude
```
## Verification Checklist
- [ ] `python3 -m SuperClaude --version` returns 4.0.9
- [ ] `python3 -m SuperClaude --version` returns 4.1.0
- [ ] `/sc:brainstorm "test"` works in Claude Code
- [ ] `SuperClaude install --list-components` shows components

View File

@ -6,7 +6,7 @@ Quick fixes to advanced diagnostics for SuperClaude Framework issues.
**Installation Verification:**
```bash
python3 -m SuperClaude --version # Should show 4.0.9
python3 -m SuperClaude --version # Should show 4.1.0
SuperClaude install --list-components
```
@ -19,7 +19,7 @@ SuperClaude install --list-components
```
**Resolution Checklist:**
- [ ] Version commands work and show 4.0.9
- [ ] Version commands work and show 4.1.0
- [ ] `/sc:` commands respond in Claude Code
- [ ] MCP servers listed: `SuperClaude install --list-components | grep mcp`

View File

@ -92,7 +92,7 @@ SuperClaude は、Claude Code が特殊な動作を実行するために読み
```shell
# Verify SuperClaude is working (primary method)
python3 -m SuperClaude --version
# Example output: SuperClaude 4.0.9
# Example output: SuperClaude 4.1.0
# Claude Code CLI version check
claude --version

View File

@ -67,7 +67,7 @@ SuperClaude 提供行为上下文文件Claude Code 通过读取这些文件
```bash
# 验证 SuperClaude 是否正常工作(主要方法)
python3 -m SuperClaude --version
# 示例输出SuperClaude 4.0.9
# 示例输出SuperClaude 4.1.0
# Claude Code CLI 版本检查
claude --version

View File

@ -67,7 +67,7 @@ SuperClaude provides behavioral context files that Claude Code reads to adopt sp
```bash
# Verify SuperClaude is working (primary method)
python3 -m SuperClaude --version
# Example output: SuperClaude 4.0.9
# Example output: SuperClaude 4.1.0
# Claude Code CLI version check
claude --version

View File

@ -5,7 +5,7 @@
### **Claude Codeを構造化開発プラットフォームに変換**
<p align="center">
<img src="https://img.shields.io/badge/version-4.0.9-blue" alt="Version">
<img src="https://img.shields.io/badge/version-4.1.0-blue" alt="Version">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome">
</p>

View File

@ -5,7 +5,7 @@
### **将Claude Code转换为结构化开发平台**
<p align="center">
<img src="https://img.shields.io/badge/version-4.0.9-blue" alt="Version">
<img src="https://img.shields.io/badge/version-4.1.0-blue" alt="Version">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome">
</p>

View File

@ -5,7 +5,7 @@
### **Transform Claude Code into a Structured Development Platform**
<p align="center">
<img src="https://img.shields.io/badge/version-4.0.9-blue" alt="Version">
<img src="https://img.shields.io/badge/version-4.1.0-blue" alt="Version">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome">
</p>

View File

@ -155,7 +155,7 @@ For actively exploited vulnerabilities or critical security issues:
| Version | Security Support | End of Support |
|---------|------------------|----------------|
| 4.0.x | ✅ Full support | TBD (current) |
| 4.1.x | ✅ Full support | TBD (current) |
| 3.x.x | ⚠️ Critical only | June 2025 |
| 2.x.x | ❌ No support | December 2024 |
| 1.x.x | ❌ No support | June 2024 |
@ -723,7 +723,7 @@ For organizations requiring dedicated security support:
**Last Updated**: December 2024 (SuperClaude Framework v4.0)
**Next Review**: March 2025 (Quarterly review cycle)
**Version**: 4.0.9 (Updated for v4 architectural changes)
**Version**: 4.1.0 (Updated for v4 architectural changes)
**Review Schedule:**
- **Quarterly Reviews**: Security policy accuracy and completeness assessment

View File

@ -17,7 +17,8 @@ from pathlib import Path
try:
__version__ = (Path(__file__).parent.parent / "VERSION").read_text().strip()
except Exception:
__version__ = "4.0.9" # Fallback
__version__ = "4.1.0" # Fallback
__author__ = "NomenAK, Mithun Gowda B"
__email__ = "anton.knoery@gmail.com"
__email__ = "anton.knoery@gmail.com, mithungowda.b7411@gmail.com"
__github__ = "NomenAK, mithun50"
__license__ = "MIT"

View File

@ -35,7 +35,7 @@ else:
try:
from setup.utils.ui import (
display_header, display_info, display_success, display_error,
display_warning, Colors
display_warning, Colors, display_authors
)
from setup.utils.logger import setup_logging, get_logger, LogLevel
from setup import DEFAULT_INSTALL_DIR
@ -100,6 +100,7 @@ Examples:
from SuperClaude import __version__
parser.add_argument("--version", action="version", version=f"SuperClaude {__version__}")
parser.add_argument("--authors", action="store_true", help="Show author information and exit")
subparsers = parser.add_subparsers(
dest="operation",
@ -204,6 +205,11 @@ def main() -> int:
operations = register_operation_parsers(subparsers, global_parser)
args = parser.parse_args()
# Handle --authors flag
if args.authors:
display_authors()
return 0
# Check for updates unless disabled
if not args.quiet and not getattr(args, 'no_update_check', False):
try:

View File

@ -1 +1 @@
4.0.9
4.1.0

View File

@ -1,6 +1,6 @@
{
"name": "@bifrost_inc/superclaude",
"version": "4.0.9",
"version": "4.1.0",
"description": "SuperClaude Framework NPM wrapper - Official Node.js wrapper for the Python SuperClaude package. Enhances Claude Code with specialized commands and AI development tools.",
"scripts": {
"postinstall": "node ./bin/install.js",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "SuperClaude"
version = "4.0.9"
version = "4.1.0"
authors = [
{name = "NomenAK", email = "anton.knoery@gmail.com"},
{name = "Mithun Gowda B", email = "mithungowda.b7411@gmail.com"}

View File

@ -8,9 +8,9 @@ from pathlib import Path
try:
__version__ = (Path(__file__).parent.parent / "VERSION").read_text().strip()
except Exception:
__version__ = "4.0.9" # Fallback
__version__ = "4.1.0" # Fallback
__author__ = "NomenAK"
__author__ = "NomenAK, Mithun Gowda B"
# Core paths
SETUP_DIR = Path(__file__).parent

View File

@ -10,7 +10,7 @@ from pathlib import Path
try:
__version__ = (Path(__file__).parent.parent.parent / "VERSION").read_text().strip()
except Exception:
__version__ = "4.0.9" # Fallback
__version__ = "4.1.0" # Fallback
def get_command_info():

View File

@ -121,8 +121,22 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis
# Explicit components specified
if args.components:
if 'all' in args.components:
return ["core", "commands", "agents", "modes", "mcp", "mcp_docs"]
return args.components
components = ["core", "commands", "agents", "modes", "mcp", "mcp_docs"]
else:
components = args.components
# If mcp is specified non-interactively, we should still ask which servers to install.
if 'mcp' in components:
selected_servers = select_mcp_servers(registry)
if not hasattr(config_manager, '_installation_context'):
config_manager._installation_context = {}
config_manager._installation_context["selected_mcp_servers"] = selected_servers
# If the user selected some servers, but didn't select mcp_docs, add it.
if selected_servers and 'mcp_docs' not in components:
components.append('mcp_docs')
return components
# Interactive two-stage selection
return interactive_component_selection(registry, config_manager)
@ -446,8 +460,8 @@ def perform_installation(components: List[str], args: argparse.Namespace, config
# Register components with installer
installer.register_components(list(component_instances.values()))
# Resolve dependencies
ordered_components = registry.resolve_dependencies(components)
# The 'components' list is already resolved, so we can use it directly.
ordered_components = components
# Setup progress tracking
progress = ProgressBar(
@ -609,13 +623,20 @@ def run(args: argparse.Namespace) -> int:
return 1
# Get components to install
components = get_components_to_install(args, registry, config_manager)
if not components:
components_to_install = get_components_to_install(args, registry, config_manager)
if not components_to_install:
logger.error("No components selected for installation")
return 1
# Validate system requirements
if not validate_system_requirements(validator, components):
# Resolve dependencies
try:
resolved_components = registry.resolve_dependencies(components_to_install)
except ValueError as e:
logger.error(f"Dependency resolution error: {e}")
return 1
# Validate system requirements for all components
if not validate_system_requirements(validator, resolved_components):
if not args.force:
logger.error("System requirements not met. Use --force to override.")
return 1
@ -632,7 +653,7 @@ def run(args: argparse.Namespace) -> int:
# Display installation plan
if not args.quiet:
display_installation_plan(components, registry, args.install_dir)
display_installation_plan(resolved_components, registry, args.install_dir)
if not args.dry_run:
if not args.yes and not confirm("Proceed with installation?", default=True):
@ -640,7 +661,7 @@ def run(args: argparse.Namespace) -> int:
return 0
# Perform installation
success = perform_installation(components, args, config_manager)
success = perform_installation(resolved_components, args, config_manager)
if success:
if not args.quiet:

View File

@ -18,6 +18,7 @@ class MCPComponent(Component):
def __init__(self, install_dir: Optional[Path] = None):
"""Initialize MCP component"""
super().__init__(install_dir)
self.installed_servers_in_session: List[str] = []
# Define MCP servers to install
self.mcp_servers = {
@ -74,6 +75,10 @@ class MCPComponent(Component):
"category": "integration"
}
def is_reinstallable(self) -> bool:
"""This component manages sub-components (servers) and should be re-run."""
return True
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
"""Check prerequisites"""
errors = []
@ -150,12 +155,12 @@ class MCPComponent(Component):
"mcp": {
"version": __version__,
"installed": True,
"servers_count": len(self.mcp_servers)
"servers_count": len(self.installed_servers_in_session)
}
},
"mcp": {
"enabled": True,
"servers": list(self.mcp_servers.keys()),
"servers": self.installed_servers_in_session,
"auto_update": False
}
}
@ -362,20 +367,35 @@ class MCPComponent(Component):
self.logger.error(error)
return False
# Install each MCP server
# Get selected servers from config
selected_servers = config.get("selected_mcp_servers", [])
if not selected_servers:
self.logger.info("No MCP servers selected for installation.")
return self._post_install()
self.logger.info(f"Installing selected MCP servers: {', '.join(selected_servers)}")
# Install each selected MCP server
installed_count = 0
failed_servers = []
self.installed_servers_in_session = []
for server_name, server_info in self.mcp_servers.items():
if self._install_mcp_server(server_info, config):
installed_count += 1
for server_name in selected_servers:
if server_name in self.mcp_servers:
server_info = self.mcp_servers[server_name]
if self._install_mcp_server(server_info, config):
installed_count += 1
self.installed_servers_in_session.append(server_name)
else:
failed_servers.append(server_name)
# Check if this is a required server
if server_info.get("required", False):
self.logger.error(f"Required MCP server {server_name} failed to install")
return False
else:
failed_servers.append(server_name)
# Check if this is a required server
if server_info.get("required", False):
self.logger.error(f"Required MCP server {server_name} failed to install")
return False
self.logger.warning(f"Unknown MCP server '{server_name}' selected for installation.")
# Verify installation
if not config.get("dry_run", False):
@ -539,7 +559,7 @@ class MCPComponent(Component):
if installed_version != expected_version:
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
# Check if Claude CLI is available
# Check if Claude CLI is available and validate installed servers
try:
result = subprocess.run(
["claude", "mcp", "list"],
@ -552,12 +572,14 @@ class MCPComponent(Component):
if result.returncode != 0:
errors.append("Could not communicate with Claude CLI for MCP server verification")
else:
# Check if required servers are installed
output = result.stdout.lower()
for server_name, server_info in self.mcp_servers.items():
if server_info.get("required", False):
if server_name.lower() not in output:
errors.append(f"Required MCP server not found: {server_name}")
claude_mcp_output = result.stdout.lower()
# Get the list of servers that should be installed from metadata
installed_servers = self.settings_manager.get_metadata_setting("mcp.servers", [])
for server_name in installed_servers:
if server_name.lower() not in claude_mcp_output:
errors.append(f"Installed MCP server '{server_name}' not found in 'claude mcp list' output.")
except Exception as e:
errors.append(f"Could not verify MCP server installation: {e}")

View File

@ -87,14 +87,12 @@ class MCPDocsComponent(Component):
# Get selected servers from config
selected_servers = config.get("selected_mcp_servers", [])
self.set_selected_servers(selected_servers)
self.component_files = self._discover_component_files()
if not selected_servers:
self.logger.info("No MCP servers selected - skipping documentation installation")
return True
self.set_selected_servers(selected_servers)
# Update component files based on selection
self.component_files = self._discover_component_files()
return self._post_install()
# Validate installation
success, errors = self.validate_prerequisites()
@ -108,7 +106,7 @@ class MCPDocsComponent(Component):
if not files_to_install:
self.logger.warning("No MCP documentation files found to install")
return True # Not an error - just no docs to install
return self._post_install()
# Copy documentation files
success_count = 0

View File

@ -46,6 +46,13 @@ class Component(ABC):
"""
pass
def is_reinstallable(self) -> bool:
"""
Whether this component should be re-installed if already present.
Useful for container-like components that can install sub-parts.
"""
return False
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
"""
Check prerequisites for this component

View File

@ -172,16 +172,13 @@ class Installer:
# Log warning but continue backup process
self.logger.warning(f"Could not backup {item.name}: {e}")
# Create archive only if there are files to backup
if any(temp_backup.iterdir()):
# shutil.make_archive adds .tar.gz automatically, so use base name without extensions
base_path = backup_dir / backup_name
shutil.make_archive(str(base_path), 'gztar', temp_backup)
else:
# Create empty backup file to indicate backup was attempted
backup_path.touch()
# Always create an archive, even if empty, to ensure it's a valid tarball
base_path = backup_dir / backup_name
shutil.make_archive(str(base_path), 'gztar', temp_backup)
if not any(temp_backup.iterdir()):
self.logger.warning(
f"No files to backup, created empty backup marker: {backup_path.name}"
f"No files to backup, created empty backup archive: {backup_path.name}"
)
self.backup_path = backup_path
@ -204,8 +201,10 @@ class Installer:
component = self.components[component_name]
# Skip if already installed and not in update mode
if component_name in self.installed_components and not config.get("update_mode"):
# Skip if already installed and not in update mode, unless component is reinstallable
if not component.is_reinstallable() and component_name in self.installed_components and not config.get("update_mode"):
self.skipped_components.add(component_name)
self.logger.info(f"Skipping already installed component: {component_name}")
return True
# Check prerequisites
@ -295,7 +294,11 @@ class Installer:
self.logger.info("Running post-installation validation...")
all_valid = True
for name in self.installed_components:
for name in self.updated_components:
if name not in self.components:
self.logger.warning(f"Cannot validate component '{name}' as it was not part of this installation session.")
continue
component = self.components[name]
success, errors = component.validate_installation()

View File

@ -2,7 +2,7 @@
"components": {
"core": {
"name": "core",
"version": "4.0.9",
"version": "4.1.0",
"description": "SuperClaude framework documentation and core files",
"category": "core",
"dependencies": [],
@ -11,7 +11,7 @@
},
"commands": {
"name": "commands",
"version": "4.0.9",
"version": "4.1.0",
"description": "SuperClaude slash command definitions",
"category": "commands",
"dependencies": ["core"],
@ -20,7 +20,7 @@
},
"mcp": {
"name": "mcp",
"version": "4.0.9",
"version": "4.1.0",
"description": "MCP server configuration management via .claude.json",
"category": "integration",
"dependencies": ["core"],
@ -29,7 +29,7 @@
},
"modes": {
"name": "modes",
"version": "4.0.9",
"version": "4.1.0",
"description": "SuperClaude behavioral modes (Brainstorming, Introspection, Task Management, Token Efficiency)",
"category": "modes",
"dependencies": ["core"],
@ -38,7 +38,7 @@
},
"mcp_docs": {
"name": "mcp_docs",
"version": "4.0.9",
"version": "4.1.0",
"description": "MCP server documentation and usage guides",
"category": "documentation",
"dependencies": ["core"],
@ -47,7 +47,7 @@
},
"agents": {
"name": "agents",
"version": "4.0.9",
"version": "4.1.0",
"description": "14 specialized AI agents with domain expertise and intelligent routing",
"category": "agents",
"dependencies": ["core"],

View File

@ -271,13 +271,54 @@ def display_header(title: str, subtitle: str = '') -> None:
title: Main title
subtitle: Optional subtitle
"""
from SuperClaude import __author__, __email__
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{title:^60}{Colors.RESET}")
if subtitle:
print(f"{Colors.WHITE}{subtitle:^60}{Colors.RESET}")
# Display authors
authors = [a.strip() for a in __author__.split(',')]
emails = [e.strip() for e in __email__.split(',')]
author_lines = []
for i in range(len(authors)):
name = authors[i]
email = emails[i] if i < len(emails) else ''
author_lines.append(f"{name} <{email}>")
authors_str = " | ".join(author_lines)
print(f"{Colors.BLUE}{authors_str:^60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}\n")
def display_authors() -> None:
"""Display author information"""
from SuperClaude import __author__, __email__, __github__
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'SuperClaude Authors':^60}{Colors.RESET}")
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*60}{Colors.RESET}\n")
authors = [a.strip() for a in __author__.split(',')]
emails = [e.strip() for e in __email__.split(',')]
github_users = [g.strip() for g in __github__.split(',')]
for i in range(len(authors)):
name = authors[i]
email = emails[i] if i < len(emails) else 'N/A'
github = github_users[i] if i < len(github_users) else 'N/A'
print(f" {Colors.BRIGHT}{name}{Colors.RESET}")
print(f" Email: {Colors.YELLOW}{email}{Colors.RESET}")
print(f" GitHub: {Colors.YELLOW}https://github.com/{github}{Colors.RESET}")
print()
print(f"{Colors.CYAN}{'='*60}{Colors.RESET}\n")
def display_info(message: str) -> None:
"""Display info message"""
print(f"{Colors.BLUE}[INFO] {message}{Colors.RESET}")

View File

@ -0,0 +1,25 @@
import pytest
from unittest.mock import patch, MagicMock
import argparse
from setup.cli.commands.install import get_components_to_install
class TestGetComponents:
@patch('setup.cli.commands.install.select_mcp_servers')
def test_get_components_to_install_interactive_mcp(self, mock_select_mcp):
# Arrange
mock_registry = MagicMock()
mock_config_manager = MagicMock()
mock_config_manager._installation_context = {}
mock_select_mcp.return_value = ['magic']
args = argparse.Namespace(components=['mcp'])
# Act
components = get_components_to_install(args, mock_registry, mock_config_manager)
# Assert
mock_select_mcp.assert_called_once()
assert 'mcp' in components
assert 'mcp_docs' in components # Should be added automatically
assert hasattr(mock_config_manager, '_installation_context')
assert mock_config_manager._installation_context['selected_mcp_servers'] == ['magic']

View File

@ -0,0 +1,58 @@
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock, ANY
import argparse
from setup.cli.commands import install
class TestInstallCommand:
@patch('setup.cli.commands.install.get_components_to_install')
@patch('setup.cli.commands.install.ComponentRegistry')
@patch('setup.cli.commands.install.ConfigService')
@patch('setup.cli.commands.install.Validator')
@patch('setup.cli.commands.install.display_installation_plan')
@patch('setup.cli.commands.install.perform_installation')
@patch('setup.cli.commands.install.confirm', return_value=True)
@patch('setup.cli.commands.install.validate_system_requirements', return_value=True)
@patch('pathlib.Path.home')
def test_run_resolves_dependencies_before_planning(
self, mock_home, mock_validate_reqs, mock_confirm, mock_perform,
mock_display, mock_validator, mock_config, mock_registry_class,
mock_get_components, tmp_path
):
# Arrange
mock_home.return_value = tmp_path
install_dir = tmp_path / ".claude"
mock_args = argparse.Namespace(
components=['mcp'],
install_dir=install_dir,
quiet=True, # to avoid calling display_header
yes=True,
force=False,
dry_run=False,
diagnose=False,
list_components=False
)
mock_registry_instance = MagicMock()
mock_registry_class.return_value = mock_registry_instance
mock_config_instance = MagicMock()
mock_config.return_value = mock_config_instance
mock_config_instance.validate_config_files.return_value = []
mock_get_components.return_value = ['mcp']
mock_registry_instance.resolve_dependencies.return_value = ['core', 'mcp']
# Act
install.run(mock_args)
# Assert
# Check that resolve_dependencies was called with the initial list
mock_registry_instance.resolve_dependencies.assert_called_once_with(['mcp'])
# Check that display_installation_plan was not called because of quiet=True
mock_display.assert_not_called()
# Check that perform_installation was called with the resolved list
mock_perform.assert_called_once_with(['core', 'mcp'], mock_args, ANY)

95
tests/test_installer.py Normal file
View File

@ -0,0 +1,95 @@
import pytest
from pathlib import Path
import shutil
import tarfile
import tempfile
from unittest.mock import MagicMock
from setup.core.installer import Installer
class TestInstaller:
def test_create_backup_empty_dir(self):
with tempfile.TemporaryDirectory() as temp_dir_str:
temp_dir = Path(temp_dir_str)
installer = Installer(install_dir=temp_dir)
backup_path = installer.create_backup()
assert backup_path is not None
assert backup_path.exists()
# This is the crucial part: check if it's a valid tar file.
# An empty file created with .touch() is not a valid tar file.
try:
with tarfile.open(backup_path, "r:gz") as tar:
members = tar.getmembers()
# An empty archive can have 0 members, or 1 member (the root dir)
if len(members) == 1:
assert members[0].name == "."
else:
assert len(members) == 0
except tarfile.ReadError as e:
pytest.fail(f"Backup file is not a valid tar.gz file: {e}")
def test_skips_already_installed_component(self):
# Create a mock component that is NOT reinstallable
mock_component = MagicMock()
mock_component.get_metadata.return_value = {'name': 'test_component'}
mock_component.is_reinstallable.return_value = False
mock_component.install.return_value = True
mock_component.validate_prerequisites.return_value = (True, [])
installer = Installer()
installer.register_component(mock_component)
# Simulate component is already installed
installer.installed_components = {'test_component'}
installer.install_component('test_component', {})
# Assert that the install method was NOT called
mock_component.install.assert_not_called()
assert 'test_component' in installer.skipped_components
def test_installs_reinstallable_component(self):
# Create a mock component that IS reinstallable
mock_component = MagicMock()
mock_component.get_metadata.return_value = {'name': 'reinstallable_component'}
mock_component.is_reinstallable.return_value = True
mock_component.install.return_value = True
mock_component.validate_prerequisites.return_value = (True, [])
installer = Installer()
installer.register_component(mock_component)
# Simulate component is already installed
installer.installed_components = {'reinstallable_component'}
installer.install_component('reinstallable_component', {})
# Assert that the install method WAS called
mock_component.install.assert_called_once()
assert 'reinstallable_component' not in installer.skipped_components
def test_post_install_validation_only_validates_updated_components(self):
# Arrange
installer = Installer()
mock_comp1 = MagicMock()
mock_comp1.get_metadata.return_value = {'name': 'comp1'}
mock_comp1.validate_installation.return_value = (True, [])
mock_comp2 = MagicMock()
mock_comp2.get_metadata.return_value = {'name': 'comp2'}
mock_comp2.validate_installation.return_value = (True, [])
installer.register_component(mock_comp1)
installer.register_component(mock_comp2)
installer.updated_components = {'comp1'}
# Act
installer._run_post_install_validation()
# Assert
mock_comp1.validate_installation.assert_called_once()
mock_comp2.validate_installation.assert_not_called()

View File

@ -0,0 +1,73 @@
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch
from setup.components.mcp import MCPComponent
class TestMCPComponent:
@patch('setup.components.mcp.MCPComponent._post_install', return_value=True)
@patch('setup.components.mcp.MCPComponent.validate_prerequisites', return_value=(True, []))
@patch('setup.components.mcp.MCPComponent._install_mcp_server')
def test_install_selected_servers_only(self, mock_install_mcp_server, mock_validate_prereqs, mock_post_install):
mock_install_mcp_server.return_value = True
component = MCPComponent(install_dir=Path('/fake/dir'))
component.installed_servers_in_session = []
# Simulate selecting only the 'magic' server
config = {
"selected_mcp_servers": ["magic"]
}
success = component._install(config)
assert success is True
assert component.installed_servers_in_session == ["magic"]
# Assert that _install_mcp_server was called exactly once
assert mock_install_mcp_server.call_count == 1
# Assert that it was called with the correct server info
called_args, _ = mock_install_mcp_server.call_args
server_info_arg = called_args[0]
assert server_info_arg['name'] == 'magic'
assert server_info_arg['npm_package'] == '@21st-dev/magic'
@patch('subprocess.run')
def test_validate_installation_success(self, mock_subprocess_run):
component = MCPComponent(install_dir=Path('/fake/dir'))
# Mock settings manager
component.settings_manager = MagicMock()
component.settings_manager.is_component_installed.return_value = True
component.settings_manager.get_component_version.return_value = component.get_metadata()['version']
component.settings_manager.get_metadata_setting.return_value = ['magic', 'playwright']
# Mock `claude mcp list` output
mock_subprocess_run.return_value.returncode = 0
mock_subprocess_run.return_value.stdout = "magic\nplaywright\n"
success, errors = component.validate_installation()
assert success is True
assert not errors
@patch('subprocess.run')
def test_validate_installation_failure(self, mock_subprocess_run):
component = MCPComponent(install_dir=Path('/fake/dir'))
# Mock settings manager
component.settings_manager = MagicMock()
component.settings_manager.is_component_installed.return_value = True
component.settings_manager.get_component_version.return_value = component.get_metadata()['version']
component.settings_manager.get_metadata_setting.return_value = ['magic', 'playwright']
# Mock `claude mcp list` output - 'playwright' is missing
mock_subprocess_run.return_value.returncode = 0
mock_subprocess_run.return_value.stdout = "magic\n"
success, errors = component.validate_installation()
assert success is False
assert len(errors) == 1
assert "playwright" in errors[0]

View File

@ -0,0 +1,35 @@
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch
from setup.components.mcp_docs import MCPDocsComponent
class TestMCPDocsComponent:
@patch('setup.components.mcp_docs.MCPDocsComponent._post_install', return_value=True)
def test_install_calls_post_install_even_if_no_docs(self, mock_post_install):
component = MCPDocsComponent(install_dir=Path('/fake/dir'))
# Simulate no servers selected
config = {
"selected_mcp_servers": []
}
success = component._install(config)
assert success is True
mock_post_install.assert_called_once()
@patch('setup.components.mcp_docs.MCPDocsComponent._post_install', return_value=True)
@patch('setup.components.mcp_docs.MCPDocsComponent.get_files_to_install', return_value=[])
@patch('setup.core.base.Component.validate_prerequisites', return_value=(True, []))
def test_install_calls_post_install_if_docs_not_found(self, mock_validate_prereqs, mock_get_files, mock_post_install):
component = MCPDocsComponent(install_dir=Path('/tmp/fake_dir'))
# Simulate a server was selected, but the doc file doesn't exist
config = {
"selected_mcp_servers": ["some_server_with_no_doc_file"]
}
success = component._install(config)
assert success is True
mock_post_install.assert_called_once()

41
tests/test_ui.py Normal file
View File

@ -0,0 +1,41 @@
import pytest
from unittest.mock import patch, MagicMock
from setup.utils.ui import display_header
import io
from setup.utils.ui import display_authors
@patch('sys.stdout', new_callable=io.StringIO)
def test_display_header_with_authors(mock_stdout):
# Mock the author and email info from SuperClaude/__init__.py
with patch('SuperClaude.__author__', "Author One, Author Two"), \
patch('SuperClaude.__email__', "one@example.com, two@example.com"):
display_header("Test Title", "Test Subtitle")
output = mock_stdout.getvalue()
assert "Test Title" in output
assert "Test Subtitle" in output
assert "Author One <one@example.com>" in output
assert "Author Two <two@example.com>" in output
assert "Author One <one@example.com> | Author Two <two@example.com>" in output
@patch('sys.stdout', new_callable=io.StringIO)
def test_display_authors(mock_stdout):
# Mock the author, email, and github info from SuperClaude/__init__.py
with patch('SuperClaude.__author__', "Author One, Author Two"), \
patch('SuperClaude.__email__', "one@example.com, two@example.com"), \
patch('SuperClaude.__github__', "user1, user2"):
display_authors()
output = mock_stdout.getvalue()
assert "SuperClaude Authors" in output
assert "Author One" in output
assert "one@example.com" in output
assert "https://github.com/user1" in output
assert "Author Two" in output
assert "two@example.com" in output
assert "https://github.com/user2" in output