mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-18 02:06:36 +00:00
Some fixes (#372)
* 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. * 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. * Add Docker support and framework enhancements - Add serena-docker.json MCP configuration - Update MCP configs and installer components - Enhance CLI commands with new functionality - Add symbols utility for framework operations - Improve UI and logging components --------- 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> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com>
This commit is contained in:
parent
5ec40fcc20
commit
00ec67c769
14
SuperClaude/MCP/configs/serena-docker.json
Normal file
14
SuperClaude/MCP/configs/serena-docker.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"serena": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-v", "${PWD}:/workspace",
|
||||
"--workdir", "/workspace",
|
||||
"python:3.11-slim",
|
||||
"bash", "-c",
|
||||
"pip install uv && uv tool install serena-ai && uv tool run serena-ai start-mcp-server --context ide-assistant --project /workspace"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
{
|
||||
"serena": {
|
||||
"command": "uvx",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server"
|
||||
"tool",
|
||||
"run",
|
||||
"serena-ai",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +498,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
actual_dir = args.install_dir.resolve()
|
||||
|
||||
if not str(actual_dir).startswith(str(expected_home)):
|
||||
print(f"\n[✗] Installation must be inside your user profile directory.")
|
||||
print(f"\n[x] Installation must be inside your user profile directory.")
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {actual_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
@ -165,7 +165,7 @@ def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> D
|
||||
return {}
|
||||
|
||||
# Display API key configuration header
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══ API Key Configuration ═══{Colors.RESET}")
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}=== API Key Configuration ==={Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n")
|
||||
|
||||
collected_keys = {}
|
||||
@ -201,9 +201,9 @@ def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
|
||||
api_key_note = " (requires API key)" if server_info.get("requires_api_key", False) else ""
|
||||
server_options.append(f"{server_key} - {description}{api_key_note}")
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 1: MCP Server Selection (Optional){Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"\n{Colors.BLUE}MCP servers extend Claude Code with specialized capabilities.{Colors.RESET}")
|
||||
print(f"{Colors.BLUE}Select servers to configure (you can always add more later):{Colors.RESET}")
|
||||
|
||||
@ -275,9 +275,9 @@ def select_framework_components(registry: ComponentRegistry, config_manager: Con
|
||||
component_options.append("mcp_docs - MCP server documentation (none selected)")
|
||||
auto_selected_mcp_docs = False
|
||||
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 2: Framework Component Selection{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}{Colors.BRIGHT}{'='*51}{Colors.RESET}")
|
||||
print(f"\n{Colors.BLUE}Select SuperClaude framework components to install:{Colors.RESET}")
|
||||
|
||||
menu = Menu("Select components (Core is recommended):", component_options, multi_select=True)
|
||||
@ -553,13 +553,13 @@ def run(args: argparse.Namespace) -> int:
|
||||
# Ensure symlink target is also within user home
|
||||
symlink_target.relative_to(expected_home)
|
||||
except ValueError:
|
||||
print(f"\n[✗] Installation must be inside your user profile directory.")
|
||||
print(f"\n[x] Installation must be inside your user profile directory.")
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {install_dir_resolved}")
|
||||
print(f" Security: Symlinks outside user directory are not allowed.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n[✗] Security validation failed: {e}")
|
||||
print(f"\n[x] Security validation failed: {e}")
|
||||
print(f" Please use a standard directory path within your user profile.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@ -469,11 +469,11 @@ def interactive_uninstall_selection(installed_components: Dict[str, str]) -> Opt
|
||||
def display_preservation_info() -> None:
|
||||
"""Show what will NOT be removed (user's custom files)"""
|
||||
print(f"\n{Colors.GREEN}{Colors.BRIGHT}Files that will be preserved:{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom commands (not in commands/sc/){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom agents (not SuperClaude agents){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom .claude.json configurations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom files in shared directories{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ Claude Code settings and other tools' configurations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom commands (not in commands/sc/){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom agents (not SuperClaude agents){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom .claude.json configurations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom files in shared directories{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ Claude Code settings and other tools' configurations{Colors.RESET}")
|
||||
|
||||
|
||||
def display_component_details(component: str, info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -562,10 +562,10 @@ def display_uninstall_plan(components: List[str], args: argparse.Namespace, info
|
||||
|
||||
# Show detailed preservation information
|
||||
print(f"\n{Colors.GREEN}{Colors.BRIGHT}Safety Guarantees - Will Preserve:{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom commands (not in commands/sc/){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's custom agents (not SuperClaude agents){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ User's .claude.json customizations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}✓ Claude Code settings and other tools' configurations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom commands (not in commands/sc/){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's custom agents (not SuperClaude agents){Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ User's .claude.json customizations{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ Claude Code settings and other tools' configurations{Colors.RESET}")
|
||||
|
||||
# Show additional preserved items
|
||||
preserved = []
|
||||
@ -578,7 +578,7 @@ def display_uninstall_plan(components: List[str], args: argparse.Namespace, info
|
||||
|
||||
if preserved:
|
||||
for item in preserved:
|
||||
print(f"{Colors.GREEN}✓ {item}{Colors.RESET}")
|
||||
print(f"{Colors.GREEN}+ {item}{Colors.RESET}")
|
||||
|
||||
if args.complete:
|
||||
print(f"\n{Colors.RED}⚠️ WARNING: Complete uninstall will remove all SuperClaude files{Colors.RESET}")
|
||||
@ -591,11 +591,11 @@ def display_uninstall_plan(components: List[str], args: argparse.Namespace, info
|
||||
for env_var in env_vars.keys():
|
||||
print(f" - {env_var}")
|
||||
if not args.no_restore_script:
|
||||
print(f"{Colors.GREEN} ✓ Restore script will be created{Colors.RESET}")
|
||||
print(f"{Colors.GREEN} + Restore script will be created{Colors.RESET}")
|
||||
else:
|
||||
print(f"{Colors.BLUE}Will preserve {len(env_vars)} API key environment variables:{Colors.RESET}")
|
||||
for env_var in env_vars.keys():
|
||||
print(f" ✓ {env_var}")
|
||||
print(f" + {env_var}")
|
||||
|
||||
print()
|
||||
|
||||
@ -770,7 +770,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
actual_dir = args.install_dir.resolve()
|
||||
|
||||
if not str(actual_dir).startswith(str(expected_home)):
|
||||
print(f"\n[✗] Installation must be inside your user profile directory.")
|
||||
print(f"\n[x] Installation must be inside your user profile directory.")
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {actual_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
@ -197,7 +197,7 @@ def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> D
|
||||
return {}
|
||||
|
||||
# Display API key configuration header
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══ API Key Configuration ═══{Colors.RESET}")
|
||||
print(f"\n{Colors.CYAN}{Colors.BRIGHT}=== API Key Configuration ==={Colors.RESET}")
|
||||
print(f"{Colors.YELLOW}New MCP servers require API keys for full functionality:{Colors.RESET}\n")
|
||||
|
||||
collected_keys = {}
|
||||
@ -388,7 +388,7 @@ def run(args: argparse.Namespace) -> int:
|
||||
actual_dir = args.install_dir.resolve()
|
||||
|
||||
if not str(actual_dir).startswith(str(expected_home)):
|
||||
print(f"\n[✗] Installation must be inside your user profile directory.")
|
||||
print(f"\n[x] Installation must be inside your user profile directory.")
|
||||
print(f" Expected prefix: {expected_home}")
|
||||
print(f" Provided path: {actual_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
@ -55,9 +55,9 @@ class MCPComponent(Component):
|
||||
"serena": {
|
||||
"name": "serena",
|
||||
"description": "Semantic code analysis and intelligent editing",
|
||||
"install_method": "uv",
|
||||
"install_command": "uv tool install --from git+https://github.com/oraios/serena serena-agent",
|
||||
"run_command": "serena start-mcp-server --context ide-assistant --project $(pwd)",
|
||||
"install_method": "github",
|
||||
"install_command": "uvx --from git+https://github.com/oraios/serena serena --help",
|
||||
"run_command": "uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context ide-assistant",
|
||||
"required": False
|
||||
},
|
||||
"morphllm-fast-apply": {
|
||||
@ -111,13 +111,13 @@ class MCPComponent(Component):
|
||||
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites"""
|
||||
errors = []
|
||||
|
||||
|
||||
# Check if Node.js is available
|
||||
try:
|
||||
result = self._run_command_cross_platform(
|
||||
["node", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
["node", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@ -125,7 +125,7 @@ class MCPComponent(Component):
|
||||
else:
|
||||
version = result.stdout.strip()
|
||||
self.logger.debug(f"Found Node.js {version}")
|
||||
|
||||
|
||||
# Check version (require 18+)
|
||||
try:
|
||||
version_num = int(version.lstrip('v').split('.')[0])
|
||||
@ -135,13 +135,13 @@ class MCPComponent(Component):
|
||||
self.logger.warning(f"Could not parse Node.js version: {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("Node.js not found - required for MCP servers")
|
||||
|
||||
|
||||
# Check if Claude CLI is available
|
||||
try:
|
||||
result = self._run_command_cross_platform(
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@ -151,13 +151,13 @@ class MCPComponent(Component):
|
||||
self.logger.debug(f"Found Claude CLI {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("Claude CLI not found - required for MCP server management")
|
||||
|
||||
|
||||
# Check if npm is available
|
||||
try:
|
||||
result = self._run_command_cross_platform(
|
||||
["npm", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
["npm", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@ -167,7 +167,23 @@ class MCPComponent(Component):
|
||||
self.logger.debug(f"Found npm {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("npm not found - required for MCP server installation")
|
||||
|
||||
|
||||
# Check if uv is available (required for Serena)
|
||||
try:
|
||||
result = self._run_command_cross_platform(
|
||||
["uv", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
self.logger.warning("uv not found - required for Serena MCP server installation")
|
||||
else:
|
||||
version = result.stdout.strip()
|
||||
self.logger.debug(f"Found uv {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
self.logger.warning("uv not found - required for Serena MCP server installation")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
@ -211,6 +227,21 @@ class MCPComponent(Component):
|
||||
self.logger.info(f"MCP server {server_name} already installed")
|
||||
return True
|
||||
|
||||
# Check if uv is available
|
||||
try:
|
||||
uv_check = self._run_command_cross_platform(
|
||||
["uv", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if uv_check.returncode != 0:
|
||||
self.logger.error(f"uv not found - required for {server_name} installation")
|
||||
return False
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
self.logger.error(f"uv not found - required for {server_name} installation")
|
||||
return False
|
||||
|
||||
if config.get("dry_run"):
|
||||
self.logger.info(f"Would install MCP server (user scope): {install_command}")
|
||||
self.logger.info(f"Would register MCP server run command: {run_command}")
|
||||
@ -229,9 +260,19 @@ class MCPComponent(Component):
|
||||
if result.returncode == 0:
|
||||
self.logger.success(f"Successfully installed MCP server (user scope): {server_name}")
|
||||
|
||||
self.logger.info(f"Registering {server_name} with Claude CLI. Run command: {run_command}")
|
||||
# For Serena, we need to handle the run command specially
|
||||
if server_name == "serena":
|
||||
# Serena needs project-specific registration, use current working directory
|
||||
current_dir = os.getcwd()
|
||||
serena_run_cmd = f"{run_command} --project {shlex.quote(current_dir)}"
|
||||
self.logger.info(f"Registering {server_name} with Claude CLI for project: {current_dir}")
|
||||
reg_cmd = ["claude", "mcp", "add", "-s", "user", "--", server_name] + shlex.split(serena_run_cmd)
|
||||
else:
|
||||
self.logger.info(f"Registering {server_name} with Claude CLI. Run command: {run_command}")
|
||||
reg_cmd = ["claude", "mcp", "add", "-s", "user", "--", server_name] + shlex.split(run_command)
|
||||
|
||||
reg_result = self._run_command_cross_platform(
|
||||
["claude", "mcp", "add", "-s", "user", "--", server_name] + shlex.split(run_command),
|
||||
reg_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
@ -256,6 +297,89 @@ class MCPComponent(Component):
|
||||
self.logger.error(f"Error installing MCP server {server_name} using uv: {e}")
|
||||
return False
|
||||
|
||||
def _install_github_mcp_server(self, server_info: Dict[str, Any], config: Dict[str, Any]) -> bool:
|
||||
"""Install a single MCP server from GitHub using uvx"""
|
||||
server_name = server_info["name"]
|
||||
install_command = server_info.get("install_command")
|
||||
run_command = server_info.get("run_command")
|
||||
|
||||
if not install_command:
|
||||
self.logger.error(f"No install_command found for GitHub-based server {server_name}")
|
||||
return False
|
||||
if not run_command:
|
||||
self.logger.error(f"No run_command found for GitHub-based server {server_name}")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.logger.info(f"Installing MCP server from GitHub: {server_name}")
|
||||
|
||||
if self._check_mcp_server_installed(server_name):
|
||||
self.logger.info(f"MCP server {server_name} already installed")
|
||||
return True
|
||||
|
||||
# Check if uvx is available
|
||||
try:
|
||||
uvx_check = self._run_command_cross_platform(
|
||||
["uvx", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if uvx_check.returncode != 0:
|
||||
self.logger.error(f"uvx not found - required for {server_name} installation")
|
||||
return False
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
self.logger.error(f"uvx not found - required for {server_name} installation")
|
||||
return False
|
||||
|
||||
if config.get("dry_run"):
|
||||
self.logger.info(f"Would install MCP server from GitHub: {install_command}")
|
||||
self.logger.info(f"Would register MCP server run command: {run_command}")
|
||||
return True
|
||||
|
||||
# Run install command to test the GitHub installation
|
||||
self.logger.debug(f"Testing GitHub installation: {install_command}")
|
||||
cmd_parts = shlex.split(install_command)
|
||||
result = self._run_command_cross_platform(
|
||||
cmd_parts,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 minutes for GitHub clone and build
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.success(f"Successfully tested GitHub MCP server: {server_name}")
|
||||
|
||||
# Register with Claude CLI using the run command
|
||||
self.logger.info(f"Registering {server_name} with Claude CLI. Run command: {run_command}")
|
||||
reg_cmd = ["claude", "mcp", "add", "-s", "user", "--", server_name] + shlex.split(run_command)
|
||||
|
||||
reg_result = self._run_command_cross_platform(
|
||||
reg_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if reg_result.returncode == 0:
|
||||
self.logger.success(f"Successfully registered {server_name} with Claude CLI.")
|
||||
return True
|
||||
else:
|
||||
error_msg = reg_result.stderr.strip() if reg_result.stderr else "Unknown error"
|
||||
self.logger.error(f"Failed to register MCP server {server_name} with Claude CLI: {error_msg}")
|
||||
return False
|
||||
else:
|
||||
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
||||
self.logger.error(f"Failed to install MCP server {server_name} from GitHub: {error_msg}\n{result.stdout}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.logger.error(f"Timeout installing MCP server {server_name} from GitHub")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error installing MCP server {server_name} from GitHub: {e}")
|
||||
return False
|
||||
|
||||
def _check_mcp_server_installed(self, server_name: str) -> bool:
|
||||
"""Check if MCP server is already installed"""
|
||||
try:
|
||||
@ -282,6 +406,8 @@ class MCPComponent(Component):
|
||||
"""Install a single MCP server"""
|
||||
if server_info.get("install_method") == "uv":
|
||||
return self._install_uv_mcp_server(server_info, config)
|
||||
elif server_info.get("install_method") == "github":
|
||||
return self._install_github_mcp_server(server_info, config)
|
||||
|
||||
server_name = server_info["name"]
|
||||
npm_package = server_info.get("npm_package")
|
||||
|
||||
@ -303,9 +303,9 @@ class Installer:
|
||||
success, errors = component.validate_installation()
|
||||
|
||||
if success:
|
||||
self.logger.info(f" ✓ {name}: Valid")
|
||||
self.logger.info(f" + {name}: Valid")
|
||||
else:
|
||||
self.logger.error(f" ✗ {name}: Invalid")
|
||||
self.logger.error(f" x {name}: Invalid")
|
||||
for error in errors:
|
||||
self.logger.error(f" - {error}")
|
||||
all_valid = False
|
||||
|
||||
@ -78,7 +78,7 @@ class CLAUDEMdService:
|
||||
User content without framework imports
|
||||
"""
|
||||
# Look for framework imports section marker
|
||||
framework_marker = "# ═══════════════════════════════════════════════════\n# SuperClaude Framework Components"
|
||||
framework_marker = "# ===================================================\n# SuperClaude Framework Components"
|
||||
|
||||
if framework_marker in content:
|
||||
user_content = content.split(framework_marker)[0].rstrip()
|
||||
@ -104,9 +104,9 @@ class CLAUDEMdService:
|
||||
sections = []
|
||||
|
||||
# Framework imports section header
|
||||
sections.append("# ═══════════════════════════════════════════════════")
|
||||
sections.append("# ===================================================")
|
||||
sections.append("# SuperClaude Framework Components")
|
||||
sections.append("# ═══════════════════════════════════════════════════")
|
||||
sections.append("# ===================================================")
|
||||
sections.append("")
|
||||
|
||||
# Add each category
|
||||
@ -197,7 +197,7 @@ class CLAUDEMdService:
|
||||
imports_by_category = {}
|
||||
|
||||
# Look for framework imports section
|
||||
framework_marker = "# ═══════════════════════════════════════════════════\n# SuperClaude Framework Components"
|
||||
framework_marker = "# ===================================================\n# SuperClaude Framework Components"
|
||||
|
||||
if framework_marker not in content:
|
||||
return imports_by_category
|
||||
@ -213,11 +213,11 @@ class CLAUDEMdService:
|
||||
line = line.strip()
|
||||
|
||||
# Skip section header lines and empty lines
|
||||
if line.startswith('# ═══') or not line:
|
||||
if line.startswith('# ===') or not line:
|
||||
continue
|
||||
|
||||
# Category header (starts with # but not the section divider)
|
||||
if line.startswith('# ') and not line.startswith('# ═══'):
|
||||
if line.startswith('# ') and not line.startswith('# ==='):
|
||||
current_category = line[2:].strip() # Remove "# "
|
||||
if current_category not in imports_by_category:
|
||||
imports_by_category[current_category] = []
|
||||
|
||||
@ -10,6 +10,7 @@ from typing import Optional, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
from .ui import Colors
|
||||
from .symbols import symbols
|
||||
|
||||
|
||||
class LogLevel(Enum):
|
||||
@ -81,7 +82,7 @@ class Logger:
|
||||
'DEBUG': '[DEBUG]',
|
||||
'INFO': '[INFO]',
|
||||
'WARNING': '[!]',
|
||||
'ERROR': '[✗]',
|
||||
'ERROR': f'[{symbols.crossmark}]',
|
||||
'CRITICAL': '[CRITICAL]'
|
||||
}
|
||||
|
||||
@ -177,7 +178,7 @@ class Logger:
|
||||
original_format = console_handler.formatter.format
|
||||
|
||||
def success_format(record):
|
||||
return f"{Colors.GREEN}[✓] {record.getMessage()}{Colors.RESET}"
|
||||
return f"{Colors.GREEN}[{symbols.checkmark}] {record.getMessage()}{Colors.RESET}"
|
||||
|
||||
console_handler.formatter.format = success_format
|
||||
self.logger.info(message, **kwargs)
|
||||
|
||||
198
setup/utils/symbols.py
Normal file
198
setup/utils/symbols.py
Normal file
@ -0,0 +1,198 @@
|
||||
"""
|
||||
Windows-compatible symbol fallbacks for SuperClaude UI
|
||||
Handles Unicode encoding issues on Windows terminals
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
|
||||
|
||||
def can_display_unicode() -> bool:
|
||||
"""
|
||||
Detect if terminal can display Unicode symbols safely
|
||||
|
||||
Returns:
|
||||
True if Unicode is safe to use, False if fallbacks needed
|
||||
"""
|
||||
# Check if we're on Windows with problematic encoding
|
||||
if platform.system() == "Windows":
|
||||
# Check console encoding
|
||||
try:
|
||||
# Test if we can encode common Unicode symbols
|
||||
test_symbols = "✓✗█░⠋═"
|
||||
test_symbols.encode(sys.stdout.encoding or 'cp1252')
|
||||
return True
|
||||
except (UnicodeEncodeError, LookupError):
|
||||
return False
|
||||
|
||||
# Check if stdout encoding supports Unicode
|
||||
encoding = getattr(sys.stdout, 'encoding', None)
|
||||
if encoding and encoding.lower() in ['utf-8', 'utf8']:
|
||||
return True
|
||||
|
||||
# Conservative fallback for unknown systems
|
||||
return False
|
||||
|
||||
|
||||
class Symbols:
|
||||
"""Cross-platform symbol definitions with Windows fallbacks"""
|
||||
|
||||
def __init__(self):
|
||||
self.unicode_safe = can_display_unicode()
|
||||
|
||||
@property
|
||||
def checkmark(self) -> str:
|
||||
"""Success checkmark symbol"""
|
||||
return "✓" if self.unicode_safe else "+"
|
||||
|
||||
@property
|
||||
def crossmark(self) -> str:
|
||||
"""Error/failure cross symbol"""
|
||||
return "✗" if self.unicode_safe else "x"
|
||||
|
||||
@property
|
||||
def block_filled(self) -> str:
|
||||
"""Filled block for progress bars"""
|
||||
return "█" if self.unicode_safe else "#"
|
||||
|
||||
@property
|
||||
def block_empty(self) -> str:
|
||||
"""Empty block for progress bars"""
|
||||
return "░" if self.unicode_safe else "-"
|
||||
|
||||
@property
|
||||
def double_line(self) -> str:
|
||||
"""Double line separator"""
|
||||
return "═" if self.unicode_safe else "="
|
||||
|
||||
@property
|
||||
def spinner_chars(self) -> str:
|
||||
"""Spinner animation characters"""
|
||||
if self.unicode_safe:
|
||||
return "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
else:
|
||||
return "|/-\\|/-\\"
|
||||
|
||||
@property
|
||||
def box_top_left(self) -> str:
|
||||
"""Box drawing: top-left corner"""
|
||||
return "╔" if self.unicode_safe else "+"
|
||||
|
||||
@property
|
||||
def box_top_right(self) -> str:
|
||||
"""Box drawing: top-right corner"""
|
||||
return "╗" if self.unicode_safe else "+"
|
||||
|
||||
@property
|
||||
def box_bottom_left(self) -> str:
|
||||
"""Box drawing: bottom-left corner"""
|
||||
return "╚" if self.unicode_safe else "+"
|
||||
|
||||
@property
|
||||
def box_bottom_right(self) -> str:
|
||||
"""Box drawing: bottom-right corner"""
|
||||
return "╝" if self.unicode_safe else "+"
|
||||
|
||||
@property
|
||||
def box_horizontal(self) -> str:
|
||||
"""Box drawing: horizontal line"""
|
||||
return "═" if self.unicode_safe else "="
|
||||
|
||||
@property
|
||||
def box_vertical(self) -> str:
|
||||
"""Box drawing: vertical line"""
|
||||
return "║" if self.unicode_safe else "|"
|
||||
|
||||
def make_separator(self, length: int) -> str:
|
||||
"""Create a separator line of specified length"""
|
||||
return self.double_line * length
|
||||
|
||||
def make_box_line(self, length: int) -> str:
|
||||
"""Create a box horizontal line of specified length"""
|
||||
return self.box_horizontal * length
|
||||
|
||||
|
||||
# Global instance for easy import
|
||||
symbols = Symbols()
|
||||
|
||||
|
||||
def safe_print(*args, **kwargs):
|
||||
"""
|
||||
Print function that handles Unicode encoding errors gracefully
|
||||
|
||||
Falls back to ASCII-safe representation if Unicode fails
|
||||
"""
|
||||
try:
|
||||
print(*args, **kwargs)
|
||||
except UnicodeEncodeError:
|
||||
# Convert arguments to safe strings
|
||||
safe_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, str):
|
||||
# Replace problematic Unicode characters
|
||||
safe_arg = (arg
|
||||
.replace("✓", "+")
|
||||
.replace("✗", "x")
|
||||
.replace("█", "#")
|
||||
.replace("░", "-")
|
||||
.replace("═", "=")
|
||||
.replace("⠋", "|")
|
||||
.replace("⠙", "/")
|
||||
.replace("⠹", "-")
|
||||
.replace("⠸", "\\")
|
||||
.replace("⠼", "|")
|
||||
.replace("⠴", "/")
|
||||
.replace("⠦", "-")
|
||||
.replace("⠧", "\\")
|
||||
.replace("⠇", "|")
|
||||
.replace("⠏", "/")
|
||||
.replace("╔", "+")
|
||||
.replace("╗", "+")
|
||||
.replace("╚", "+")
|
||||
.replace("╝", "+")
|
||||
.replace("║", "|")
|
||||
)
|
||||
safe_args.append(safe_arg)
|
||||
else:
|
||||
safe_args.append(str(arg))
|
||||
|
||||
# Try printing with safe arguments
|
||||
try:
|
||||
print(*safe_args, **kwargs)
|
||||
except UnicodeEncodeError:
|
||||
# Last resort: encode to ASCII with replacement
|
||||
final_args = []
|
||||
for arg in safe_args:
|
||||
if isinstance(arg, str):
|
||||
final_args.append(arg.encode('ascii', 'replace').decode('ascii'))
|
||||
else:
|
||||
final_args.append(str(arg))
|
||||
print(*final_args, **kwargs)
|
||||
|
||||
|
||||
def format_with_symbols(text: str) -> str:
|
||||
"""
|
||||
Replace Unicode symbols in text with Windows-compatible alternatives
|
||||
"""
|
||||
if symbols.unicode_safe:
|
||||
return text
|
||||
|
||||
# Replace symbols with safe alternatives
|
||||
replacements = {
|
||||
"✓": symbols.checkmark,
|
||||
"✗": symbols.crossmark,
|
||||
"█": symbols.block_filled,
|
||||
"░": symbols.block_empty,
|
||||
"═": symbols.double_line,
|
||||
"╔": symbols.box_top_left,
|
||||
"╗": symbols.box_top_right,
|
||||
"╚": symbols.box_bottom_left,
|
||||
"╝": symbols.box_bottom_right,
|
||||
"║": symbols.box_vertical,
|
||||
}
|
||||
|
||||
for unicode_char, safe_char in replacements.items():
|
||||
text = text.replace(unicode_char, safe_char)
|
||||
|
||||
return text
|
||||
@ -9,6 +9,7 @@ import shutil
|
||||
import getpass
|
||||
from typing import List, Optional, Any, Dict, Union
|
||||
from enum import Enum
|
||||
from .symbols import symbols, safe_print, format_with_symbols
|
||||
|
||||
# Try to import colorama for cross-platform color support
|
||||
try:
|
||||
@ -88,8 +89,8 @@ class ProgressBar:
|
||||
|
||||
# Calculate filled and empty portions
|
||||
filled_width = int(self.width * current / self.total) if self.total > 0 else self.width
|
||||
filled = '█' * filled_width
|
||||
empty = '░' * (self.width - filled_width)
|
||||
filled = symbols.block_filled * filled_width
|
||||
empty = symbols.block_empty * (self.width - filled_width)
|
||||
|
||||
# Calculate elapsed time and ETA
|
||||
elapsed = time.time() - self.start_time
|
||||
@ -118,7 +119,7 @@ class ProgressBar:
|
||||
if len(plain_line) > max_length:
|
||||
progress_line = progress_line[:max_length] + "..."
|
||||
|
||||
print(progress_line, end='', flush=True)
|
||||
safe_print(progress_line, end='', flush=True)
|
||||
|
||||
def increment(self, message: str = '') -> None:
|
||||
"""
|
||||
@ -326,7 +327,7 @@ def display_info(message: str) -> None:
|
||||
|
||||
def display_success(message: str) -> None:
|
||||
"""Display success message"""
|
||||
print(f"{Colors.GREEN}[✓] {message}{Colors.RESET}")
|
||||
safe_print(f"{Colors.GREEN}[{symbols.checkmark}] {message}{Colors.RESET}")
|
||||
|
||||
|
||||
def display_warning(message: str) -> None:
|
||||
@ -336,7 +337,7 @@ def display_warning(message: str) -> None:
|
||||
|
||||
def display_error(message: str) -> None:
|
||||
"""Display error message"""
|
||||
print(f"{Colors.RED}[✗] {message}{Colors.RESET}")
|
||||
safe_print(f"{Colors.RED}[{symbols.crossmark}] {message}{Colors.RESET}")
|
||||
|
||||
|
||||
def display_step(step: int, total: int, message: str) -> None:
|
||||
@ -410,11 +411,11 @@ def prompt_api_key(service_name: str, env_var_name: str) -> Optional[str]:
|
||||
if not confirm("", default=False):
|
||||
return None
|
||||
|
||||
print(f"{Colors.GREEN}[✓] {env_var_name} configured{Colors.RESET}")
|
||||
safe_print(f"{Colors.GREEN}[{symbols.checkmark}] {env_var_name} configured{Colors.RESET}")
|
||||
return api_key
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.YELLOW}[SKIPPED] {env_var_name}{Colors.RESET}")
|
||||
safe_print(f"\n{Colors.YELLOW}[SKIPPED] {env_var_name}{Colors.RESET}")
|
||||
return None
|
||||
|
||||
|
||||
@ -444,7 +445,7 @@ class StatusSpinner:
|
||||
"""
|
||||
self.message = message
|
||||
self.spinning = False
|
||||
self.chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
self.chars = symbols.spinner_chars
|
||||
self.current = 0
|
||||
|
||||
def start(self) -> None:
|
||||
@ -454,7 +455,7 @@ class StatusSpinner:
|
||||
def spin():
|
||||
while self.spinning:
|
||||
char = self.chars[self.current % len(self.chars)]
|
||||
print(f"\r{Colors.BLUE}{char} {self.message}{Colors.RESET}", end='', flush=True)
|
||||
safe_print(f"\r{Colors.BLUE}{char} {self.message}{Colors.RESET}", end='', flush=True)
|
||||
self.current += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
@ -474,10 +475,10 @@ class StatusSpinner:
|
||||
self.thread.join(timeout=0.2)
|
||||
|
||||
# Clear spinner line
|
||||
print(f"\r{' ' * (len(self.message) + 5)}\r", end='')
|
||||
|
||||
safe_print(f"\r{' ' * (len(self.message) + 5)}\r", end='')
|
||||
|
||||
if final_message:
|
||||
print(final_message)
|
||||
safe_print(final_message)
|
||||
|
||||
|
||||
def format_size(size_bytes: int) -> str:
|
||||
|
||||
@ -202,10 +202,10 @@ class UpdateChecker:
|
||||
update_cmd = self.get_update_command()
|
||||
|
||||
# Display banner
|
||||
print(f"\n{Colors.CYAN}╔════════════════════════════════════════════════╗{Colors.RESET}")
|
||||
print(f"\n{Colors.CYAN}+================================================+{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}║{Colors.YELLOW} 🚀 Update Available: {self.current_version} → {latest} {Colors.CYAN}║{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}║{Colors.GREEN} Run: {update_cmd:<30} {Colors.CYAN}║{Colors.RESET}")
|
||||
print(f"{Colors.CYAN}╚════════════════════════════════════════════════╝{Colors.RESET}\n")
|
||||
print(f"{Colors.CYAN}+================================================+{Colors.RESET}\n")
|
||||
|
||||
if auto_update:
|
||||
return True
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user