Add missing install.sh script (#483)

* feat: add missing install.sh script referenced in README\n\n- Create comprehensive installation script with POSIX compatibility\n- Add interactive and non-interactive installation modes\n- Include prerequisites checking and MCP server setup guidance\n- Replace echo -e with printf for better POSIX compliance

* fix: resolve linting errors in install_mcp.py and clean_command_names.py

Fix multiple ruff linting errors to ensure CI/CD pipeline passes:

- install_mcp.py: Remove unused pathlib.Path import, replace bare except
  with specific exception types (ValueError, IndexError), remove
  extraneous f-string prefixes on lines without placeholders
- clean_command_names.py: Remove unused os import, convert f-strings
  without placeholders to regular strings
- pyproject.toml: Exclude docs/ directory from ruff checks to avoid
  N999 module naming violations in documentation templates

All linting checks now pass successfully.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* style: apply ruff format to Python source files

Apply ruff formatting rules to CLI and scripts modules to ensure
consistent code style across the codebase.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ci): remove incompatible pip cache from quick-check workflow

## Problem
GitHub Actions was failing with error:
"Cache folder path is retrieved for pip but doesn't exist on disk:
/home/runner/.cache/pip. This likely indicates that there are no
dependencies to cache."

## Root Cause
The quick-check.yml workflow specified `cache: 'pip'` in the Python
setup step, but the workflow uses UV (not pip) for package management
via `uv pip install --system -e ".[dev]"`.

UV uses its own cache directory (~/.cache/uv), so the pip cache path
was never created, causing the error.

This was a migration oversight:
- When UV was adopted as the project standard (commit 00706f0), the
  CLAUDE.md established "CRITICAL: Never use pip directly" rule
- The test.yml workflow was created correctly without pip cache
- The quick-check.yml workflow incorrectly included pip cache from
  initial creation (commit 8c0559c) and was not updated during migration

## Solution
Remove `cache: 'pip'` line to align with:
- Project's UV-first architecture (CLAUDE.md)
- test.yml workflow (which runs successfully without pip cache)
- readme-quality-check.yml workflow (no cache needed)

Note: publish-pypi.yml intentionally uses pip cache as it directly
runs `python -m pip install` commands, which is correct for that workflow.

## Impact
-  Eliminates GitHub Actions cache warning
-  Aligns all UV-based workflows consistently
-  Follows project standards documented in CLAUDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
BlackBear
2025-11-14 11:33:04 +09:00
committed by GitHub
parent 1b14d06537
commit 18c0e4e127
11 changed files with 423 additions and 50 deletions

View File

@@ -9,7 +9,6 @@ import os
import platform
import shlex
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import click
@@ -137,7 +136,7 @@ def check_prerequisites() -> Tuple[bool, List[str]]:
errors.append(
f"Node.js version {version} found, but version 18+ required"
)
except:
except (ValueError, IndexError):
pass
except (subprocess.TimeoutExpired, FileNotFoundError):
errors.append("Node.js not found - required for npm-based MCP servers")
@@ -173,7 +172,9 @@ def check_mcp_server_installed(server_name: str) -> bool:
return False
def prompt_for_api_key(server_name: str, env_var: str, description: str) -> Optional[str]:
def prompt_for_api_key(
server_name: str, env_var: str, description: str
) -> Optional[str]:
"""Prompt user for API key if needed."""
click.echo(f"\n🔑 MCP server '{server_name}' requires an API key")
click.echo(f" Environment variable: {env_var}")
@@ -189,14 +190,14 @@ def prompt_for_api_key(server_name: str, env_var: str, description: str) -> Opti
api_key = click.prompt(f" Enter {env_var}", hide_input=True)
return api_key
else:
click.echo(f" ⚠️ Proceeding without {env_var} - server may not function properly")
click.echo(
f" ⚠️ Proceeding without {env_var} - server may not function properly"
)
return None
def install_mcp_server(
server_info: Dict,
scope: str = "user",
dry_run: bool = False
server_info: Dict, scope: str = "user", dry_run: bool = False
) -> bool:
"""
Install a single MCP server using modern Claude Code API.
@@ -227,7 +228,7 @@ def install_mcp_server(
api_key = prompt_for_api_key(
server_name,
api_key_env,
server_info.get("api_key_description", f"API key for {server_name}")
server_info.get("api_key_description", f"API key for {server_name}"),
)
if api_key:
@@ -260,13 +261,10 @@ def install_mcp_server(
return True
try:
click.echo(f" Running: claude mcp add --transport {transport} {server_name} -- {command}")
result = _run_command(
cmd,
capture_output=True,
text=True,
timeout=120
click.echo(
f" Running: claude mcp add --transport {transport} {server_name} -- {command}"
)
result = _run_command(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
click.echo(f" ✅ Successfully installed: {server_name}")
@@ -310,7 +308,7 @@ def list_available_servers():
def install_mcp_servers(
selected_servers: Optional[List[str]] = None,
scope: str = "user",
dry_run: bool = False
dry_run: bool = False,
) -> Tuple[bool, str]:
"""
Install MCP servers for Claude Code.
@@ -347,8 +345,12 @@ def install_mcp_servers(
server_options = []
for key, info in MCP_SERVERS.items():
api_note = f" (requires {info['api_key_env']})" if "api_key_env" in info else ""
server_options.append(f"{info['name']:25} - {info['description']}{api_note}")
api_note = (
f" (requires {info['api_key_env']})" if "api_key_env" in info else ""
)
server_options.append(
f"{info['name']:25} - {info['description']}{api_note}"
)
for i, option in enumerate(server_options, 1):
click.echo(f" {i}. {option}")
@@ -358,7 +360,7 @@ def install_mcp_servers(
selection = click.prompt(
"Select servers to install (comma-separated numbers, or 0 for all)",
default="0"
default="0",
)
if selection.strip() == "0":
@@ -367,7 +369,9 @@ def install_mcp_servers(
try:
indices = [int(x.strip()) for x in selection.split(",")]
server_list = list(MCP_SERVERS.keys())
servers_to_install = [server_list[i-1] for i in indices if 0 < i <= len(server_list)]
servers_to_install = [
server_list[i - 1] for i in indices if 0 < i <= len(server_list)
]
except (ValueError, IndexError):
return False, "Invalid selection"
@@ -394,6 +398,6 @@ def install_mcp_servers(
return False, message
else:
message = f"\n✅ Successfully installed {installed_count} MCP server(s)!\n"
message += f"\n Use 'claude mcp list' to see all installed servers"
message += f"\n Use '/mcp' in Claude Code to check server status"
message += "\n Use 'claude mcp list' to see all installed servers"
message += "\n Use '/mcp' in Claude Code to check server status"
return True, message

View File

@@ -91,11 +91,18 @@ def install(target: str, force: bool, list_only: bool):
@main.command()
@click.option("--servers", "-s", multiple=True, help="Specific MCP servers to install")
@click.option("--list", "list_only", is_flag=True, help="List available MCP servers")
@click.option(
"--list", "list_only", is_flag=True, help="List available MCP servers"
"--scope",
default="user",
type=click.Choice(["local", "project", "user"]),
help="Installation scope",
)
@click.option(
"--dry-run",
is_flag=True,
help="Show what would be installed without actually installing",
)
@click.option("--scope", default="user", type=click.Choice(["local", "project", "user"]), help="Installation scope")
@click.option("--dry-run", is_flag=True, help="Show what would be installed without actually installing")
def mcp(servers, list_only, scope, dry_run):
"""
Install and manage MCP servers for Claude Code
@@ -118,7 +125,7 @@ def mcp(servers, list_only, scope, dry_run):
success, message = install_mcp_servers(
selected_servers=list(servers) if servers else None,
scope=scope,
dry_run=dry_run
dry_run=dry_run,
)
click.echo(message)

View File

@@ -14,7 +14,6 @@ Exit Codes:
1 - Error (directory not found or processing failed)
"""
import os
import re
import sys
from pathlib import Path
@@ -55,20 +54,20 @@ def clean_name_attributes(content: str) -> Tuple[str, bool]:
"""
# Pattern to match 'name: value' in frontmatter
# Matches: name: value, name : value, NAME: value (case-insensitive)
name_pattern = re.compile(r'^\s*name\s*:\s*.*$', re.MULTILINE | re.IGNORECASE)
name_pattern = re.compile(r"^\s*name\s*:\s*.*$", re.MULTILINE | re.IGNORECASE)
# Check if modification is needed
if not name_pattern.search(content):
return content, False
# Remove name attributes
cleaned = name_pattern.sub('', content)
cleaned = name_pattern.sub("", content)
# Clean up multiple consecutive newlines (max 2)
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
# Remove trailing whitespace while preserving final newline
cleaned = cleaned.rstrip() + '\n' if cleaned.strip() else ''
cleaned = cleaned.rstrip() + "\n" if cleaned.strip() else ""
return cleaned, True
@@ -92,7 +91,7 @@ def process_commands_directory(commands_dir: Path) -> int:
error_count = 0
print(f"🔍 Scanning: {commands_dir}")
print(f"{'='*60}")
print(f"{'=' * 60}")
# Process all .md files
for md_file in sorted(commands_dir.glob("*.md")):
@@ -100,14 +99,14 @@ def process_commands_directory(commands_dir: Path) -> int:
try:
# Read file content
content = md_file.read_text(encoding='utf-8')
content = md_file.read_text(encoding="utf-8")
# Clean name attributes
cleaned_content, was_modified = clean_name_attributes(content)
if was_modified:
# Write cleaned content back
md_file.write_text(cleaned_content, encoding='utf-8')
md_file.write_text(cleaned_content, encoding="utf-8")
modified_count += 1
print(f"✅ Modified: {md_file.name}")
else:
@@ -117,8 +116,8 @@ def process_commands_directory(commands_dir: Path) -> int:
error_count += 1
print(f"❌ Error: {md_file.name} - {e}", file=sys.stderr)
print(f"{'='*60}")
print(f"📊 Summary:")
print("=" * 60)
print("📊 Summary:")
print(f" • Processed: {processed_count} files")
print(f" • Modified: {modified_count} files")
print(f" • Errors: {error_count} files")
@@ -151,7 +150,7 @@ def main() -> int:
print("\n❌ Cleanup failed with errors", file=sys.stderr)
return 1
print(f"\n✅ Cleanup completed successfully")
print("\n✅ Cleanup completed successfully")
return 0
except FileNotFoundError as e:
@@ -160,6 +159,7 @@ def main() -> int:
except Exception as e:
print(f"❌ Unexpected error: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1