
Lsp Setup
- 143 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
Configure Language Server Protocol clients and servers so editors, agents, and IDEs get accurate diagnostics, go-to-definition, and refactor support across polyglot codebases during implementation.
About
Covers LSP setup for amplihack environments: install and configure language servers, connect editor or agent clients, tune workspace roots and file watchers, and verify diagnostics, completion, and navigation work reliably across multi-language repositories.
- Language Server Protocol client setup
- Editor and agent bridge configuration
- Diagnostics and navigation enablement
- Polyglot workspace compatibility
- Improves in-IDE agent accuracy
Lsp Setup by the numbers
- 143 all-time installs (skills.sh)
- +1 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #223 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill lsp-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 143 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
What it does
Configure Language Server Protocol clients and servers so editors, agents, and IDEs get accurate diagnostics, go-to-definition, and refactor support across polyglot codebases during implementation.
Files
LSP Auto-Configuration Skill
Manual control and troubleshooting for Language Server Protocol (LSP) configuration.
Important: LSP is Configured Automatically
You probably don't need this skill! LSP is configured automatically when you run amplihack claude.
When amplihack launches Claude Code, it automatically:
- Detects programming languages in your project
- Sets
ENABLE_LSP_TOOL=1environment variable - Installs required LSP plugins via Claude Code plugin marketplace
- Configures project-specific settings in
.env
You'll see: 📡 LSP: Detected 3 language(s): python, javascript... when amplihack starts Claude Code.
When to Use This Skill
Use `/lsp-setup` only when you need to:
- Check Status: Verify LSP configuration is working (
/lsp-setup --status-only) - Troubleshoot Issues: Diagnose why code intelligence isn't working
- Force Reconfiguration: Rebuild LSP config after manual changes (
/lsp-setup --force) - Add New Languages: Configure LSP for newly added languages
Don't use this skill if: LSP is already working! The automatic setup handles 99% of cases.
Overview
The LSP Setup skill provides manual control over the same LSP auto-configuration that happens automatically at launch. It supports 16 popular programming languages out of the box and gives you direct access to configuration, status checking, and troubleshooting.
What LSP Provides to You
When LSP is properly configured, Claude Code gains powerful code intelligence capabilities that enhance the AI's understanding of your codebase:
Real-Time Code Intelligence
Type Information - Claude can see the exact types of variables, functions, and classes:
# Claude can hover over 'user' and see: Type: User (class from models.py)
user = get_current_user()Go to Definition - Claude can jump to where symbols are defined:
# Claude can navigate from 'authenticate()' call to its definition in auth.py
result = authenticate(credentials)Code Diagnostics - Claude receives real-time error detection from LSP servers:
# Pyright reports: "Set" is not accessed
from typing import List, Set # Claude sees this warningHow Claude Uses LSP
Better Code Understanding - Instead of guessing types and behavior, Claude gets precise information from LSP servers about:
- Function signatures and return types
- Class hierarchies and inheritance
- Import paths and module structure
- Errors and warnings before runtime
Smarter Suggestions - With LSP data, Claude provides:
- Accurate refactoring recommendations
- Type-safe code completions
- Precise error fixes
- Context-aware code generation
Example Workflow:
You: "Fix the type error in user_service.py"
Without LSP: Claude reads the file, guesses at types, may miss subtle issues
With LSP: Claude receives diagnostic:
Line 42: Expected type 'User | None', got 'str'
→ Claude provides exact fix based on actual type informationWhat You'll Notice
After /lsp-setup configures your project:
1. More Accurate Responses - Claude's code suggestions match your actual types and APIs 2. Faster Debugging - Claude sees the same errors your IDE would show 3. Better Refactoring - Claude can safely rename variables across files using LSP references 4. Improved Navigation - Claude can find definitions, usages, and implementations precisely
Important Note
LSP enhances Claude's capabilities _behind the scenes_. You don't interact with LSP directly - it makes Claude smarter about your code automatically.
When to Use This Skill
Use /lsp-setup when you:
- Start working on a new project in Claude Code
- Add a new programming language to an existing project
- Experience missing or incorrect code intelligence features
- Want to verify your LSP configuration is correct
- Need to troubleshoot LSP server connection issues
How It Works
LSP Architecture - Three Layers
Claude Code's LSP system uses a three-layer architecture that must all be configured for LSP features to work:
Layer 1: System LSP Binaries (User-installed)
- LSP server executables installed on your system via npm, brew, rustup, etc.
- Example:
npm install -g pyrightinstalls the Pyright LSP server binary - These are the actual language analysis engines
Layer 2: Claude Code LSP Plugins (Installed via cclsp)
- Claude Code plugins that connect to Layer 1 binaries
- Installed using:
npx cclsp install <server-name> - The
cclsptool uses theclaude-code-lspsplugin marketplace - These act as bridges between Claude Code and LSP servers
Layer 3: Project Configuration (.env file)
- Project-specific settings: virtual environments, project roots, etc.
- Must include
ENABLE_LSP_TOOL=1to activate LSP features - Stored in
.envat project root
Important: cclsp and claude-code-lsps work together. cclsp is the installation tool, claude-code-lsps is the plugin marketplace it uses. They are complementary, not alternatives.
The Skill's 4-Phase Process
The /lsp-setup skill automates the workflow from npx cclsp@latest setup:
Phase 1: Language Detection
Scans your project directory to identify programming languages based on file extensions and framework markers. Detects 16 languages including Python, TypeScript, JavaScript, Rust, Go, Java, and more.
Phase 2: LSP Configuration
Generates the appropriate LSP server configuration for each detected language. Checks if:
1. System LSP binaries are installed (Layer 1) 2. Claude Code plugins are installed (Layer 2)
Provides installation guidance if either is missing. NEVER auto-installs - user has full control.
Phase 3: Project Configuration
Creates or updates .env file with project-specific LSP settings (Layer 3):
- Workspace-specific options (Python virtual environments, Node.js project roots, etc.)
- ENABLE_LSP_TOOL=1 (required for LSP features to activate)
Phase 4: Verification
Tests each LSP server connection and reports status. Provides actionable guidance for any configuration issues.
Usage
Basic Usage
/lsp-setupDetects all languages in your project and configures LSP servers automatically.
Check Status Only
/lsp-setup --status-onlyReports current LSP configuration and server availability without making changes.
Force Reconfiguration
/lsp-setup --forceRegenerates LSP configuration even if valid configuration already exists.
Specific Languages
/lsp-setup --languages python,typescriptConfigures LSP servers only for specified languages.
Manual Plugin Management
If you need to manage Claude Code LSP plugins directly, use the cclsp command:
# Install a plugin (Layer 2)
npx cclsp install pyright
# List installed plugins
npx cclsp list
# Remove a plugin
npx cclsp remove pyright
# Full setup workflow (what /lsp-setup automates)
npx cclsp@latest setupThe /lsp-setup skill automates the npx cclsp@latest setup workflow, adding intelligent language detection and project-specific configuration.
Supported Languages
| Language | LSP Server | System Binary Installation (Layer 1) | Claude Code Plugin (Layer 2) |
|---|---|---|---|
| Python | pyright | npm install -g pyright | npx cclsp install pyright |
| TypeScript | vtsls | npm install -g @vtsls/language-server | npx cclsp install vtsls |
| JavaScript | vtsls | npm install -g @vtsls/language-server | npx cclsp install vtsls |
| Rust | rust-analyzer | rustup component add rust-analyzer | npx cclsp install rust-analyzer |
| Go | gopls | go install golang.org/x/tools/gopls@latest | npx cclsp install gopls |
| Java | jdtls | Download from eclipse.org/jdtls | npx cclsp install jdtls |
| C/C++ | clangd | brew install llvm (macOS) / apt install clangd (Linux) | npx cclsp install clangd |
| C# | omnisharp | Download from omnisharp.net | npx cclsp install omnisharp |
| Ruby | ruby-lsp | gem install ruby-lsp | npx cclsp install ruby-lsp |
| PHP | phpactor | composer global require phpactor/phpactor | npx cclsp install phpactor |
| Bash | bash-language-server | npm install -g bash-language-server | npx cclsp install bash-language-server |
| YAML | yaml-language-server | npm install -g yaml-language-server | npx cclsp install yaml-language-server |
| JSON | vscode-json-languageserver | npm install -g vscode-json-languageserver | npx cclsp install vscode-json-languageserver |
| HTML | vscode-html-languageserver | npm install -g vscode-html-languageserver | npx cclsp install vscode-html-languageserver |
| CSS | vscode-css-languageserver | npm install -g vscode-css-languageserver | npx cclsp install vscode-css-languageserver |
| Markdown | marksman | brew install marksman (macOS) / Download from GitHub | npx cclsp install marksman |
Note: Both Layer 1 (system binary) and Layer 2 (Claude Code plugin) must be installed for LSP features to work.
Example: Python Project Setup
$ cd my-python-project
$ /lsp-setup
[LSP Setup] Detecting languages...
✓ Found: Python (23 files)
✓ Found: YAML (2 files)
✓ Found: Markdown (1 file)
[LSP Setup] Configuring LSP servers...
✓ pyright: Installed at /usr/local/bin/pyright
✓ yaml-language-server: Installed at /usr/local/bin/yaml-language-server
✓ marksman: Installed at /usr/local/bin/marksman
[LSP Setup] Configuring project...
✓ Created .env with LSP configuration
✓ Detected Python virtual environment: .venv
✓ Configured pyright to use .venv/bin/python
[LSP Setup] Verifying connections...
✓ pyright: Connected (Python 3.11.5)
✓ yaml-language-server: Connected
✓ marksman: Connected
Configuration complete! LSP servers ready.Example: Polyglot Project Setup
$ cd my-fullstack-app
$ /lsp-setup
[LSP Setup] Detecting languages...
✓ Found: TypeScript (45 files)
✓ Found: Python (12 files)
✓ Found: Rust (8 files)
✓ Found: JSON (6 files)
[LSP Setup] Configuring LSP servers...
✓ vtsls: Installed
✓ pyright: Installed
✗ rust-analyzer: Not found
[LSP Setup] Installation guidance:
To install rust-analyzer:
$ rustup component add rust-analyzer
Would you like to continue with available servers? [Y/n] y
[LSP Setup] Configuring project...
✓ Created .env with LSP configuration
✓ Detected Node.js project root: ./frontend
✓ Detected Python project root: ./backend
✓ Detected Rust workspace: ./services
[LSP Setup] Verifying connections...
✓ vtsls: Connected (TypeScript 5.3.3)
✓ pyright: Connected (Python 3.11.5)
⚠ rust-analyzer: Skipped (not installed)
Configuration complete! 2/3 LSP servers ready.
Run `rustup component add rust-analyzer` to enable Rust support.How to Verify LSP is Working
After running /lsp-setup, here's how to confirm LSP is providing code intelligence to Claude:
Method 1: Check for Diagnostics
Ask Claude to analyze a file with intentional errors:
You: "What issues do you see in src/main.py?"
If LSP is working: Claude will report specific diagnostics from Pyright
→ "Line 15: 'name' is not accessed"
→ "Line 23: Expected type 'int', got 'str'"
If LSP is NOT working: Claude only sees what's in the file content
→ Generic observations about code style
→ No specific type errors or warningsMethod 2: Request Type Information
Ask Claude about types in your code:
You: "What's the type of the 'user' variable in auth.py line 42?"
If LSP is working: Claude provides exact type from LSP
→ "Type: User | None (from models.User)"
If LSP is NOT working: Claude guesses based on context
→ "It appears to be a User object based on the code"Method 3: Test Navigation
Ask Claude to find definitions:
You: "Where is the authenticate() function defined?"
If LSP is working: Claude uses LSP goToDefinition
→ "Defined in src/auth/service.py:156"
If LSP is NOT working: Claude searches file contents
→ "I found it by searching for 'def authenticate'"Method 4: Check Status Command
Run the status check:
/lsp-setup --status-onlyExpected Output (LSP Working):
[LSP Setup] Configuration Status:
✓ Python (pyright): Connected
- System Binary: /usr/local/bin/pyright-langserver
- Plugin: Installed and active
- Project Config: .env configured with ENABLE_LSP_TOOL=1
✓ TypeScript (vtsls): Connected
- System Binary: /usr/local/bin/vtsls
- Plugin: Installed and active
- Project Config: .env configured with ENABLE_LSP_TOOL=1
Overall Status: ✓ All LSP servers ready (2/2)Problem Output (LSP NOT Working):
[LSP Setup] Configuration Status:
✗ Python (pyright): Not Connected
- System Binary: Not found
- Plugin: Not installed
- Project Config: Missing ENABLE_LSP_TOOL=1
Issue: Run 'npm install -g pyright' and 'npx cclsp install pyright'What Success Looks Like
When LSP is working properly, you'll notice:
1. Claude mentions specific line numbers when discussing errors 2. Claude provides exact types instead of guessing 3. Claude sees warnings/errors before code runs (like your IDE does) 4. Claude can navigate code structure using LSP's understanding
What Failure Looks Like
When LSP is NOT working, you'll notice:
1. Claude only sees file contents - no type information or diagnostics 2. Claude makes educated guesses about types and behavior 3. Claude doesn't mention errors until you run the code 4. Claude searches text instead of using semantic understanding
Troubleshooting
Issue: LSP server not found
Symptom: "rust-analyzer: Not found" during configuration
Solution: Install the LSP server using the provided installation command
$ rustup component add rust-analyzer
$ /lsp-setup --force # Reconfigure after installationIssue: LSP server crashes on startup
Symptom: "pyright: Connection failed" during verification
Solution: Check LSP server logs and verify installation
# Check if server is properly installed
$ which pyright
/usr/local/bin/pyright
# Test server manually
$ pyright --version
pyright 1.1.332
# Check Claude Code LSP logs
$ cat ~/.claude-code/lsp-logs/pyright.logIssue: Wrong Python interpreter used
Symptom: Import errors despite packages being installed in virtual environment
Solution: Verify .env configuration points to correct Python interpreter
# Check current configuration
$ cat .env | grep PYTHON
# Should show:
LSP_PYTHON_INTERPRETER=/path/to/.venv/bin/python
# If incorrect, update manually or run:
$ /lsp-setup --forceIssue: TypeScript project not detected
Symptom: No TypeScript LSP configuration despite .ts files present
Solution: Ensure tsconfig.json exists in project root
# Create minimal tsconfig.json
$ cat > tsconfig.json << EOF
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true
}
}
EOF
$ /lsp-setup --forceIssue: Multiple Python versions causing conflicts
Symptom: "Module not found" errors when LSP server uses wrong Python version
Solution: Explicitly set Python interpreter in .env
# Edit .env manually
LSP_PYTHON_INTERPRETER=/usr/bin/python3.11
# Or activate the correct virtual environment before running
$ source .venv/bin/activate
$ /lsp-setup --forceConfiguration Files
.env (Project Root)
The skill creates or updates .env with LSP-specific configuration:
# LSP Configuration - Auto-generated by /lsp-setup
ENABLE_LSP_TOOL=1 # REQUIRED: Activates LSP features in Claude Code
# Project-specific settings (Layer 3)
LSP_PYTHON_INTERPRETER=/path/to/.venv/bin/python
LSP_NODE_PROJECT_ROOT=/path/to/frontend
LSP_RUST_WORKSPACE=/path/to/services
LSP_GO_MODULE=/path/to/go.mod
# Language Server Paths (auto-detected)
LSP_PYRIGHT_PATH=/usr/local/bin/pyright
LSP_VTSLS_PATH=/usr/local/bin/vtsls
LSP_RUST_ANALYZER_PATH=/usr/local/bin/rust-analyzerCritical: ENABLE_LSP_TOOL=1 must be present for LSP features to work. Without it, LSP servers won't be activated.
Claude Code LSP Configuration
The skill automatically updates Claude Code's LSP configuration. No manual editing required.
Integration with Claude Code
Once configured, Claude Code automatically:
- Provides code completion based on project context
- Shows real-time diagnostics (errors, warnings)
- Enables "Go to Definition" navigation
- Offers inline documentation on hover
- Suggests intelligent refactorings
Best Practices
1. Run at project start: Configure LSP servers when you first open a project 2. Update after adding languages: Rerun /lsp-setup when adding new language files 3. Commit `.env`: Include LSP configuration in version control for team consistency 4. Verify after installation: Use --status-only to check server availability 5. Keep servers updated: Regularly update LSP servers to latest versions
Technical Details
Skill Type: Interactive command-driven workflow
Dependencies: None (pure skill, no external packages)
Execution Time: 2-10 seconds depending on project size
Persistence: Configuration stored in .env and Claude Code settings
Supported Platforms: macOS, Linux, WSL
Related Skills
environment-setup- General development environment configurationdependency-manager- Package and dependency managementproject-init- New project initialization
Maintenance
The LSP Setup skill requires no manual maintenance. It automatically adapts to your project structure and detects language changes on each run.
To update LSP server definitions or add new languages, see the developer documentation in README.md.
---
Execution Instructions (For Claude Code)
When this skill is activated via /lsp-setup or activation keywords, execute the following workflow:
Phase 1: Language Detection
from pathlib import Path
from lsp_setup import LanguageDetector
# Detect languages in current project
detector = LanguageDetector()
project_root = Path.cwd()
languages = detector.detect_languages(project_root)
# Report findings
print(f"Detected {len(languages)} language(s):")
for lang in languages:
print(f" - {lang.language}: {lang.file_count} files")Phase 2: Check Current Status
from lsp_setup import StatusTracker
# Get current LSP setup status
language_names = [lang.language for lang in languages]
tracker = StatusTracker(project_root, language_names)
status = tracker.get_full_status()
# Show status
print(tracker.generate_user_guidance())Phase 3: Configure LSP (If Needed)
from lsp_setup import LSPConfigurator
# Configure .env file
configurator = LSPConfigurator(project_root)
# Enable LSP
if not configurator.is_lsp_enabled():
configurator.enable_lsp()
print("✓ Enabled LSP in .env")Phase 4: Install Plugins (If Needed)
from lsp_setup import PluginManager
# Install Claude Code plugins for detected languages
manager = PluginManager()
# Check prerequisites
if not manager.check_npx_available():
print("⚠ npx not found. Install Node.js first:")
print(" macOS: brew install node")
print(" Linux: sudo apt install nodejs")
exit(1)
# Install plugins for languages with missing Layer 2
for lang_name in language_names:
layer_2_status = status["layer_2"][lang_name]
if not layer_2_status["installed"]:
print(f"Installing {lang_name} plugin...")
success = manager.install_plugin(lang_name)
if success:
print(f"✓ {lang_name} plugin installed")
else:
print(f"✗ Failed to install {lang_name} plugin")
print(f" {layer_2_status.get('install_guide', 'See docs')}")Phase 5: Final Status Report
# Recheck status after installation
final_status = tracker.get_full_status()
if final_status["overall_ready"]:
print("\n✅ LSP setup complete! All layers configured.")
else:
print("\n⚠ LSP partially configured. Next steps:")
print(tracker.generate_user_guidance())Command-Line Arguments (Optional)
Support these arguments if provided by user:
--status-only: Skip installation, just report current status--force: Reinstall all plugins even if already installed--languages <lang1,lang2>: Configure only specific languages--dry-run: Show what would be done without making changes
"""LSP Auto-Configuration Module.
Provides automatic Language Server Protocol (LSP) configuration for Claude Code.
Philosophy:
- Ruthless simplicity - Standard library only where possible
- Self-contained modules with clear public APIs
- Zero-BS implementation - Every function works
- Regeneratable from specification
Public API (the "studs"):
LanguageDetector: Detect programming languages in project
LSPConfigurator: Configure .env file for LSP
PluginManager: Manage Claude Code LSP plugins
StatusTracker: Track three-layer LSP setup status
"""
from .language_detector import LanguageDetection, LanguageDetector
from .lsp_configurator import LSPConfigurator
from .mcp_configurator import CCLSPConfig, MCPConfigurator
from .plugin_manager import PluginInstallResult, PluginManager
from .status_tracker import LayerStatus, StatusTracker
__all__ = [
"LanguageDetector",
"LanguageDetection",
"LSPConfigurator",
"PluginManager",
"PluginInstallResult",
"StatusTracker",
"LayerStatus",
"MCPConfigurator",
"CCLSPConfig",
]
__version__ = "0.1.0"
"""Language detection module for LSP Auto-Configuration.
Detects programming languages in project directory by scanning file extensions
and project markers (package.json, Cargo.toml, etc.).
Philosophy:
- Single responsibility: Language detection only
- Standard library only (Path, os, glob)
- Self-contained and regeneratable
Public API:
LanguageDetection: Result of language detection scan
LanguageDetector: Main language detection class
"""
import fnmatch
from dataclasses import dataclass
from pathlib import Path
@dataclass
class LanguageDetection:
"""Result of language detection scan."""
language: str # e.g., "python", "typescript"
file_count: int # Number of files detected
primary: bool # True if this is primary project language
markers: list[str] # Framework markers found (e.g., "package.json")
class LanguageDetector:
"""Detects programming languages in project directory.
Scans project files and identifies languages based on:
- File extensions (.py, .ts, .rs, etc.)
- Project markers (package.json, Cargo.toml, etc.)
- Framework-specific files
Example:
>>> detector = LanguageDetector(Path("/path/to/project"))
>>> languages = detector.detect_languages()
>>> for lang in languages:
... print(f"{lang}: {languages[lang]} files")
"""
# Language definitions: extensions and project markers
LANGUAGE_DEFINITIONS = {
"python": {
"extensions": [".py", ".pyi", ".pyw"],
"markers": ["setup.py", "pyproject.toml", "requirements.txt", "Pipfile"],
},
"typescript": {
"extensions": [".ts", ".tsx"],
"markers": ["tsconfig.json", "package.json"],
},
"javascript": {
"extensions": [".js", ".jsx", ".mjs", ".cjs"],
"markers": ["package.json", "package-lock.json"],
},
"rust": {
"extensions": [".rs"],
"markers": ["Cargo.toml", "Cargo.lock"],
},
"go": {
"extensions": [".go"],
"markers": ["go.mod", "go.sum"],
},
"java": {
"extensions": [".java"],
"markers": ["pom.xml", "build.gradle", "build.gradle.kts"],
},
"cpp": {
"extensions": [".cpp", ".cc", ".cxx", ".h", ".hpp"],
"markers": ["CMakeLists.txt", "Makefile"],
},
"ruby": {
"extensions": [".rb"],
"markers": ["Gemfile", "Rakefile"],
},
"php": {
"extensions": [".php"],
"markers": ["composer.json"],
},
"csharp": {
"extensions": [".cs"],
"markers": [".csproj", ".sln"],
},
"kotlin": {
"extensions": [".kt", ".kts"],
"markers": ["build.gradle.kts"],
},
"swift": {
"extensions": [".swift"],
"markers": ["Package.swift"],
},
"scala": {
"extensions": [".scala"],
"markers": ["build.sbt"],
},
"lua": {
"extensions": [".lua"],
"markers": [],
},
"elixir": {
"extensions": [".ex", ".exs"],
"markers": ["mix.exs"],
},
"haskell": {
"extensions": [".hs", ".lhs"],
"markers": ["stack.yaml", "cabal.project"],
},
}
# Common directories to ignore
IGNORED_DIRS = {
"node_modules",
".git",
".venv",
"venv",
"env",
"__pycache__",
".pytest_cache",
"dist",
"build",
"target",
".idea",
".vscode",
"coverage",
".next",
".nuxt",
}
def __init__(self, project_root: Path, max_languages: int | None = None):
"""Initialize language detector.
Args:
project_root: Path to project root directory
max_languages: Optional limit on number of languages to detect
"""
self.project_root = Path(project_root)
self.max_languages = max_languages
self._gitignore_patterns: list[str] | None = None
# Load gitignore patterns on init
self._load_gitignore()
def detect_languages(self, max_languages: int | None = None) -> dict[str, int]:
"""Detect all programming languages in project.
Args:
max_languages: Optional limit on number of languages to return (overrides init value)
Returns:
Dict mapping language name to file count, sorted by count descending
Example:
>>> detector.detect_languages()
{'python': 23, 'yaml': 2}
"""
language_counts: dict[str, int] = {}
# Scan all files in project
for file_path in self._scan_project_files():
# Check each language's extensions
for language, config in self.LANGUAGE_DEFINITIONS.items():
if self._is_language_file(file_path, language):
language_counts[language] = language_counts.get(language, 0) + 1
# Sort by count descending
sorted_languages = dict(sorted(language_counts.items(), key=lambda x: x[1], reverse=True))
# Apply max limit from parameter or init
limit = max_languages if max_languages is not None else self.max_languages
if limit is not None:
sorted_languages = dict(list(sorted_languages.items())[:limit])
return sorted_languages
def detect_languages_with_confidence(self) -> dict[str, int]:
"""Detect languages with confidence scores (file counts).
Returns:
Dict mapping language name to file count (confidence score)
Example:
>>> detector.detect_languages_with_confidence()
{'python': 23, 'javascript': 12}
"""
return self.detect_languages()
def get_primary_language(self) -> str | None:
"""Identify the primary language of the project.
Primary language determined by:
1. Presence of framework markers (package.json, Cargo.toml, etc.)
2. Language with most files
Returns:
Primary language name, or None if no languages detected
Example:
>>> detector.get_primary_language()
'python'
"""
# First check for framework markers
for language, config in self.LANGUAGE_DEFINITIONS.items():
for marker in config.get("markers", []):
marker_path = self.project_root / marker
if marker_path.exists():
return language
# Fall back to language with most files
languages = self.detect_languages()
if languages:
return next(iter(languages)) # First key (highest count)
return None
def detect_language_frameworks(self, language: str) -> list[str]:
"""Detect frameworks for a specific language.
Args:
language: Language identifier (e.g., "python", "typescript")
Returns:
List of framework markers found
Example:
>>> detector.detect_language_frameworks("python")
['pyproject.toml', 'setup.py']
"""
if language not in self.LANGUAGE_DEFINITIONS:
return []
markers_found = []
markers = self.LANGUAGE_DEFINITIONS[language].get("markers", [])
for marker in markers:
marker_path = self.project_root / marker
if marker_path.exists():
markers_found.append(marker)
return markers_found
def _scan_project_files(self) -> list[Path]:
"""Scan project directory for all files, respecting ignored dirs.
Returns:
List of file paths to analyze
"""
files = []
for path in self.project_root.rglob("*"):
# Skip if path is file and not in ignored directory
if path.is_file():
# Check if any parent is in ignored dirs
if not self._should_ignore_path(path):
files.append(path)
return files
def _should_ignore_path(self, path: Path) -> bool:
"""Check if path should be ignored based on ignored dirs and gitignore.
Args:
path: Path to check
Returns:
True if path should be ignored
"""
# Check if any parent directory is in ignored list
parts = path.relative_to(self.project_root).parts
for part in parts[:-1]: # Exclude filename itself
if part in self.IGNORED_DIRS:
return True
# Check gitignore patterns if available
if self._gitignore_patterns is not None:
relative_path = str(path.relative_to(self.project_root))
filename = path.name
for pattern in self._gitignore_patterns:
# Handle directory patterns (ending with /)
if pattern.endswith("/"):
dir_pattern = pattern[:-1]
# Check if path is inside this directory
if dir_pattern in parts:
return True
# Handle wildcard patterns
elif fnmatch.fnmatch(filename, pattern) or fnmatch.fnmatch(relative_path, pattern):
return True
return False
def _load_gitignore(self) -> None:
"""Load .gitignore patterns for filtering."""
gitignore_path = self.project_root / ".gitignore"
if not gitignore_path.exists():
self._gitignore_patterns = []
return
patterns = []
with open(gitignore_path) as f:
for line in f:
line = line.strip()
# Skip comments and empty lines
if line and not line.startswith("#"):
patterns.append(line)
self._gitignore_patterns = patterns
def _is_language_file(self, file_path: Path, language: str) -> bool:
"""Check if file belongs to specified language.
Args:
file_path: Path to file
language: Language to check
Returns:
True if file extension matches language
"""
if language not in self.LANGUAGE_DEFINITIONS:
return False
extensions = self.LANGUAGE_DEFINITIONS[language]["extensions"]
return file_path.suffix in extensions
def get_file_extensions_for_language(self, language: str) -> list[str]:
"""Get file extensions for a specific language.
Args:
language: Language identifier
Returns:
List of file extensions (e.g., ['.py', '.pyi'])
"""
if language not in self.LANGUAGE_DEFINITIONS:
return []
return self.LANGUAGE_DEFINITIONS[language]["extensions"]
def get_extensions_for_language(self, language: str) -> list[str]:
"""Alias for get_file_extensions_for_language.
Args:
language: Language identifier
Returns:
List of file extensions
"""
return self.get_file_extensions_for_language(language)
def is_language_file(self, file_path: Path, language: str) -> bool:
"""Public API for checking if file is a specific language.
Args:
file_path: Path to file
language: Language to check
Returns:
True if file belongs to language
"""
return self._is_language_file(file_path, language)
__all__ = ["LanguageDetection", "LanguageDetector"]
"""LSP Configurator module for managing .env file configuration.
Handles Layer 3 of the LSP setup: Project configuration via .env file.
Philosophy:
- Single responsibility: .env file management only
- Standard library only (Path, re)
- Atomic file operations for safety
- Self-contained and regeneratable
Public API:
LSPConfigurator: Main class for LSP configuration management
"""
import re
import shutil
from pathlib import Path
from typing import Any
class LSPConfigurator:
"""Manages LSP configuration in project .env file.
Handles creating, reading, and updating the .env file to enable/disable
LSP support in Claude Code.
Example:
>>> configurator = LSPConfigurator(Path("/path/to/project"))
>>> configurator.enable_lsp()
True
>>> configurator.is_lsp_enabled()
True
"""
LSP_ENABLE_KEY = "ENABLE_LSP_TOOL"
ENV_FILE_NAME = ".env"
def __init__(self, project_root: Path):
"""Initialize LSP configurator.
Args:
project_root: Path to project root directory
"""
self.project_root = Path(project_root)
self.env_file_path = self.project_root / self.ENV_FILE_NAME
def enable_lsp(self) -> bool:
"""Enable LSP in .env file.
Creates or updates .env file to set ENABLE_LSP_TOOL=1.
Preserves existing environment variables.
Creates automatic backup if .env exists.
Returns:
True if successful
Example:
>>> configurator.enable_lsp()
True
"""
# Backup existing file before modification
if self.env_file_path.exists():
self.backup_env_file()
return self.set_env_variable(self.LSP_ENABLE_KEY, "1")
def disable_lsp(self) -> bool:
"""Disable LSP in .env file.
Updates .env file to set ENABLE_LSP_TOOL=0.
Returns:
True if successful
Example:
>>> configurator.disable_lsp()
True
"""
return self.set_env_variable(self.LSP_ENABLE_KEY, "0")
def is_lsp_enabled(self) -> bool:
"""Check if LSP is currently enabled in .env file.
Returns:
True if ENABLE_LSP_TOOL=1, False otherwise
Example:
>>> configurator.is_lsp_enabled()
True
"""
if not self.env_file_path.exists():
return False
env_vars = self.get_all_env_variables()
value = env_vars.get(self.LSP_ENABLE_KEY, "0")
return value == "1"
def get_env_file_path(self) -> Path:
"""Get the path to the .env file.
Returns:
Path to .env file
Example:
>>> configurator.get_env_file_path()
PosixPath('/project/.env')
"""
return self.env_file_path
def backup_env_file(self) -> Path | None:
"""Create a backup of the current .env file.
Returns:
Path to backup file, or None if .env doesn't exist
Example:
>>> backup_path = configurator.backup_env_file()
"""
if not self.env_file_path.exists():
return None
# Create backup with .backup suffix (not replacing .env)
backup_path = self.project_root / (self.ENV_FILE_NAME + ".backup")
shutil.copy2(self.env_file_path, backup_path)
return backup_path
def validate_env_file_syntax(self) -> tuple[bool, list[str]]:
"""Validate .env file syntax.
Returns:
Tuple of (is_valid, list_of_errors)
Example:
>>> valid, errors = configurator.validate_env_file_syntax()
>>> if not valid:
... print(f"Errors: {errors}")
"""
if not self.env_file_path.exists():
return True, []
errors = []
content = self.env_file_path.read_text()
for line_num, line in enumerate(content.split("\n"), 1):
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith("#"):
continue
# Check for valid key=value format
if "=" not in line:
errors.append(f"Line {line_num}: Missing '=' in assignment")
continue
key, _ = line.split("=", 1)
if not key or not key.strip():
errors.append(f"Line {line_num}: Empty variable name")
return len(errors) == 0, errors
def get_all_env_variables(self) -> dict[str, str]:
"""Get all environment variables from .env file.
Returns:
Dict mapping variable names to values
Example:
>>> vars = configurator.get_all_env_variables()
>>> print(vars['API_KEY'])
"""
if not self.env_file_path.exists():
return {}
env_vars = {}
content = self.env_file_path.read_text()
for line in content.split("\n"):
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith("#"):
continue
# Parse key=value
if "=" in line:
key, value = line.split("=", 1)
env_vars[key.strip()] = value.strip()
return env_vars
def set_env_variable(self, key: str, value: str) -> bool:
"""Set an environment variable in .env file.
Preserves comments and empty lines. Updates existing key or appends new one.
Args:
key: Variable name
value: Variable value
Returns:
True if successful
Example:
>>> configurator.set_env_variable("API_KEY", "secret")
True
"""
try:
# Read existing content or start fresh
if self.env_file_path.exists():
lines = self.env_file_path.read_text().split("\n")
else:
lines = []
# Look for existing key
key_pattern = re.compile(f"^{re.escape(key)}\\s*=")
key_found = False
for i, line in enumerate(lines):
if key_pattern.match(line.strip()):
# Update existing key
lines[i] = f"{key}={value}"
key_found = True
break
# Add new key if not found
if not key_found:
# Remove trailing empty lines
while lines and not lines[-1].strip():
lines.pop()
# Add new key
lines.append(f"{key}={value}")
# Ensure single newline at end
content = "\n".join(lines)
if content and not content.endswith("\n"):
content += "\n"
# Write atomically
self.env_file_path.write_text(content)
return True
except OSError:
# Handle permission errors, disk full, etc.
return False
def remove_env_variable(self, key: str) -> bool:
"""Remove an environment variable from .env file.
Args:
key: Variable name to remove
Returns:
True if successful
Example:
>>> configurator.remove_env_variable("OLD_KEY")
True
"""
if not self.env_file_path.exists():
return True # Already doesn't exist
try:
lines = self.env_file_path.read_text().split("\n")
key_pattern = re.compile(f"^{re.escape(key)}\\s*=")
# Filter out lines matching the key
filtered_lines = [line for line in lines if not key_pattern.match(line.strip())]
# Write back
content = "\n".join(filtered_lines)
if content and not content.endswith("\n"):
content += "\n"
self.env_file_path.write_text(content)
return True
except OSError:
return False
def get_lsp_status_summary(self) -> dict[str, Any]:
"""Get summary of LSP configuration status.
Returns:
Dict with configuration status information
Example:
>>> status = configurator.get_lsp_status_summary()
>>> print(status['enabled'])
"""
return {
"enabled": self.is_lsp_enabled(),
"env_file_exists": self.env_file_path.exists(),
"env_file_path": str(self.env_file_path),
"env_variable_count": len(self.get_all_env_variables()),
}
def get_status_summary(self) -> dict[str, Any]:
"""Alias for get_lsp_status_summary.
Returns:
Dict with configuration status information
"""
return self.get_lsp_status_summary()
def validate_env_syntax(self) -> bool:
"""Validate .env file syntax (simplified return).
Returns:
True if valid, False otherwise
Example:
>>> configurator.validate_env_syntax()
True
"""
is_valid, _ = self.validate_env_file_syntax()
return is_valid
__all__ = ["LSPConfigurator"]
"""
MCP Configurator for cclsp LSP integration.
Philosophy:
- Configure Claude Code to use cclsp as MCP server
- Generate cclsp.json configuration file
- Add MCP server to Claude Code settings.json
- Zero-BS: Real configuration, no stubs
Public API:
MCPConfigurator: Configure cclsp MCP server for Claude Code
"""
import json
import subprocess
from pathlib import Path
from typing import Any
__all__ = ["MCPConfigurator", "CCLSPConfig"]
class CCLSPConfig:
"""cclsp.json configuration generator."""
# Language to LSP server mapping
LANGUAGE_SERVERS = {
"python": {"extensions": ["py"], "command": ["pylsp"], "rootDir": "."},
"javascript": {
"extensions": ["js", "jsx"],
"command": ["typescript-language-server", "--stdio"],
"rootDir": ".",
},
"typescript": {
"extensions": ["ts", "tsx"],
"command": ["typescript-language-server", "--stdio"],
"rootDir": ".",
},
"rust": {"extensions": ["rs"], "command": ["rust-analyzer"], "rootDir": "."},
"go": {"extensions": ["go"], "command": ["gopls"], "rootDir": "."},
"java": {"extensions": ["java"], "command": ["jdtls"], "rootDir": "."},
"cpp": {
"extensions": ["cpp", "cxx", "cc", "h", "hpp"],
"command": ["clangd"],
"rootDir": ".",
},
"c": {"extensions": ["c", "h"], "command": ["clangd"], "rootDir": "."},
"ruby": {"extensions": ["rb"], "command": ["solargraph", "stdio"], "rootDir": "."},
"php": {"extensions": ["php"], "command": ["phpactor", "language-server"], "rootDir": "."},
}
@classmethod
def generate_config(cls, languages: list[str]) -> dict[str, Any]:
"""Generate cclsp.json configuration for detected languages."""
servers = []
for lang in languages:
if lang in cls.LANGUAGE_SERVERS:
servers.append(cls.LANGUAGE_SERVERS[lang])
return {"servers": servers}
class MCPConfigurator:
"""Configure Claude Code MCP server for cclsp LSP integration."""
def __init__(self, project_root: Path):
"""Initialize MCP configurator.
Args:
project_root: Project root directory
"""
self.project_root = Path(project_root)
self.cclsp_config_file = self.project_root / "cclsp.json"
self.claude_settings = self.project_root / ".claude" / "settings.json"
def generate_cclsp_config(self, languages: list[str]) -> bool:
"""Generate cclsp.json configuration file.
Args:
languages: List of language names to configure
Returns:
True if successful
"""
try:
config = CCLSPConfig.generate_config(languages)
# Write cclsp.json
self.cclsp_config_file.write_text(json.dumps(config, indent=2) + "\n")
return True
except OSError:
return False
def add_mcp_server_to_claude(self) -> bool:
"""Add cclsp MCP server to Claude Code settings.json.
Returns:
True if successful
"""
try:
# Ensure .claude directory exists
self.claude_settings.parent.mkdir(parents=True, exist_ok=True)
# Load or create settings
if self.claude_settings.exists():
settings = json.loads(self.claude_settings.read_text())
else:
settings = {}
# Add mcpServers section if not present
if "mcpServers" not in settings:
settings["mcpServers"] = {}
# Add cclsp server configuration
settings["mcpServers"]["cclsp"] = {"command": "npx", "args": ["cclsp"]}
# Write back
self.claude_settings.write_text(json.dumps(settings, indent=2) + "\n")
return True
except (OSError, json.JSONDecodeError):
return False
def is_cclsp_configured(self) -> bool:
"""Check if cclsp MCP server is configured.
Returns:
True if configured
"""
if not self.claude_settings.exists():
return False
try:
settings = json.loads(self.claude_settings.read_text())
return "cclsp" in settings.get("mcpServers", {})
except (OSError, json.JSONDecodeError):
return False
def check_cclsp_available(self) -> bool:
"""Check if cclsp is available via npx.
Returns:
True if cclsp can be run
"""
try:
result = subprocess.run(["npx", "cclsp", "--version"], capture_output=True, timeout=5)
# cclsp doesn't have --version, but if it runs, it's available
return result.returncode in [0, 1] # May return 1 for unknown flag
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
"""
Plugin Manager for Claude Code LSP plugins.
Philosophy:
- Wraps npx cclsp commands for plugin management
- User-guided installation (never auto-installs system binaries)
- Retry logic with exponential backoff
- Clear, actionable error messages
Public API:
PluginManager: Manages Claude Code LSP plugin lifecycle
install_plugin: Install a plugin via npx cclsp
uninstall_plugin: Remove a plugin
is_plugin_installed: Check if plugin is installed
list_installed_plugins: Get all installed plugins
"""
import re
import subprocess
import time
from dataclasses import dataclass
__all__ = ["PluginManager", "PluginInstallResult"]
@dataclass
class PluginInstallResult:
"""Result of plugin installation operation."""
success: bool
plugin_name: str
error: str | None = None
output: str | None = None
class PluginManager:
"""Manages Claude Code LSP plugin installation and lifecycle."""
def __init__(self, max_retries: int = 3, timeout: int = 120):
"""
Initialize plugin manager.
Args:
max_retries: Maximum retry attempts for failed operations
timeout: Timeout in seconds for subprocess calls
"""
self.max_retries = max_retries
self.timeout = timeout
def _validate_plugin_name(self, plugin_name: str) -> None:
"""
Validate plugin name contains only safe characters.
Args:
plugin_name: Name to validate
Raises:
ValueError: If plugin name contains unsafe characters
"""
if not re.match(r"^[a-zA-Z0-9_-]+$", plugin_name):
raise ValueError(f"Invalid plugin name: {plugin_name}")
def check_npx_available(self) -> bool:
"""
Check if npx is available on the system.
Returns:
True if npx is available, False otherwise
"""
try:
result = subprocess.run(["npx", "--version"], capture_output=True, text=True, timeout=5)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def is_plugin_installed(self, plugin_name: str) -> bool:
"""
Check if a plugin is installed.
Args:
plugin_name: Name of the plugin (e.g., "python", "typescript")
Returns:
True if plugin is installed, False otherwise
"""
self._validate_plugin_name(plugin_name)
installed = self.list_installed_plugins()
return plugin_name in installed
def list_installed_plugins(self) -> list[str]:
"""
List all installed Claude Code LSP plugins.
Returns:
List of installed plugin names
"""
if not self.check_npx_available():
return []
try:
result = subprocess.run(
["npx", "cclsp", "list"], capture_output=True, text=True, timeout=self.timeout
)
if result.returncode == 0:
# Parse plugin list from stdout (one per line)
plugins = [
line.strip() for line in result.stdout.strip().split("\n") if line.strip()
]
return plugins
return []
except (subprocess.TimeoutExpired, Exception):
return []
def install_plugin(self, plugin_name: str, dry_run: bool = False) -> bool:
"""
Install a Claude Code LSP plugin via npx cclsp.
Args:
plugin_name: Name of plugin to install (e.g., "python")
dry_run: If True, don't actually install, just check
Returns:
True if installation successful, False otherwise
"""
self._validate_plugin_name(plugin_name)
if not self.check_npx_available():
return False
if dry_run:
# Just check if plugin is already installed (validation happens in is_plugin_installed)
return not self.is_plugin_installed(plugin_name)
# Check if already installed
if self.is_plugin_installed(plugin_name):
return True
# Install with retry logic
for attempt in range(self.max_retries):
try:
result = subprocess.run(
["npx", "cclsp", "install", plugin_name],
capture_output=True,
text=True,
timeout=self.timeout,
)
if result.returncode == 0:
return True
# Retry on failure (except last attempt)
if attempt < self.max_retries - 1:
time.sleep(2**attempt) # Exponential backoff
except subprocess.TimeoutExpired:
if attempt < self.max_retries - 1:
time.sleep(2**attempt)
continue
return False
return False
def install_multiple_plugins(
self, plugin_names: list[str], dry_run: bool = False
) -> tuple[list[str], list[str]]:
"""
Install multiple plugins.
Args:
plugin_names: List of plugin names to install
dry_run: If True, don't actually install
Returns:
Tuple of (successful_plugins, failed_plugins)
"""
successful = []
failed = []
for plugin in plugin_names:
if self.install_plugin(plugin, dry_run=dry_run):
successful.append(plugin)
else:
failed.append(plugin)
return successful, failed
def uninstall_plugin(self, plugin_name: str) -> bool:
"""
Uninstall a Claude Code LSP plugin.
Args:
plugin_name: Name of plugin to uninstall
Returns:
True if uninstallation successful, False otherwise
"""
self._validate_plugin_name(plugin_name)
if not self.check_npx_available():
return False
# Check if plugin is installed (validation happens in is_plugin_installed)
if not self.is_plugin_installed(plugin_name):
return True # Already uninstalled
try:
result = subprocess.run(
["npx", "cclsp", "uninstall", plugin_name],
capture_output=True,
text=True,
timeout=self.timeout,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, Exception):
return False
def get_plugin_info(self, plugin_name: str) -> dict | None:
"""
Get information about a plugin.
Args:
plugin_name: Name of the plugin
Returns:
Dict with plugin info, or None if not found
"""
if not self.is_plugin_installed(plugin_name):
return None
return {"name": plugin_name, "installed": True, "source": "npx cclsp"}
def update_plugin(self, plugin_name: str) -> bool:
"""
Update a plugin to latest version.
Args:
plugin_name: Name of plugin to update
Returns:
True if update successful, False otherwise
"""
# Uninstall then reinstall to get latest version
if self.uninstall_plugin(plugin_name):
return self.install_plugin(plugin_name)
return False
LSP Auto-Configuration Module
Developer documentation for the Claude Code LSP Setup skill.
Overview
The LSP Setup module provides automatic Language Server Protocol (LSP) configuration for Claude Code. It follows the brick philosophy: self-contained, regeneratable modules with clear public APIs.
LSP Architecture - Three Layers
Claude Code's LSP system requires three layers to be configured:
1. Layer 1: System LSP Binaries - LSP server executables installed via npm, brew, rustup, etc. 2. Layer 2: Claude Code LSP Plugins - Plugins installed via npx cclsp install <server> that bridge Claude Code to Layer 1 binaries 3. Layer 3: Project Configuration - .env file with ENABLE_LSP_TOOL=1 and project-specific settings
Critical Understanding: cclsp and claude-code-lsps work together, not as alternatives:
cclsp= The installation command-line toolclaude-code-lsps= The plugin marketplace thatcclspuses- They are complementary components of the same system
What This Module Does
This module automates the npx cclsp@latest setup workflow, adding:
- Intelligent language detection across 16 languages
- Automatic detection of Layer 1 and Layer 2 installation status
- User-guided installation (NEVER auto-installs system binaries)
- Project-specific configuration generation (Layer 3)
- Connection verification and troubleshooting guidance
Architecture
Module Structure
.claude/skills/lsp-setup/
├── __init__.py # Public API exports
├── SKILL.md # User-facing documentation
├── README.md # This file (developer documentation)
├── USAGE_EXAMPLES.md # Practical usage examples
├── language_detector.py # Language detection logic
├── lsp_configurator.py # LSP server configuration
├── plugin_manager.py # LSP plugin lifecycle management
├── status_tracker.py # Configuration status tracking
├── config/
│ ├── language_definitions.json # Supported languages and LSP servers
│ └── lsp_server_templates.json # LSP server configuration templates
├── tests/
│ ├── test_language_detector.py
│ ├── test_lsp_configurator.py
│ ├── test_plugin_manager.py
│ ├── test_status_tracker.py
│ └── fixtures/
│ ├── sample_python_project/
│ ├── sample_typescript_project/
│ └── sample_polyglot_project/
└── examples/
├── basic_usage.py
├── advanced_usage.py
└── troubleshooting.pyDesign Philosophy
Brick Principles Applied:
1. Single Responsibility: Each module handles one aspect of LSP configuration 2. Self-Contained: No external dependencies beyond standard library and Claude Code SDK 3. Clear Public API: __all__ defines stable interface 4. Regeneratable: Can be rebuilt from this specification 5. Isolated Testing: All tests contained within module
Zero-BS Implementation:
- No stub functions or placeholders
- Every function works or doesn't exist
- Real LSP server detection, not mock data
- Actual file I/O, not simulated operations
Module Specifications
1. language_detector.py
Purpose: Detect programming languages in project directory
Public API:
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class LanguageDetection:
"""Result of language detection scan"""
language: str # e.g., "python", "typescript"
file_count: int # Number of files detected
primary: bool # True if this is primary project language
markers: List[str] # Framework markers found (e.g., "package.json")
class LanguageDetector:
"""Detects programming languages in project directory"""
def detect_languages(self, project_root: Path) -> List[LanguageDetection]:
"""
Scan project directory and identify all languages present.
Args:
project_root: Path to project root directory
Returns:
List of LanguageDetection objects, sorted by file count (descending)
Example:
>>> detector = LanguageDetector()
>>> languages = detector.detect_languages(Path("/path/to/project"))
>>> for lang in languages:
... print(f"{lang.language}: {lang.file_count} files")
python: 23 files
yaml: 2 files
"""
def get_primary_language(self, project_root: Path) -> Optional[LanguageDetection]:
"""
Identify the primary language of the project.
Primary language determined by:
1. Language with most files
2. Presence of framework markers (package.json, Cargo.toml, etc.)
3. Language-specific configuration files
Returns:
LanguageDetection for primary language, or None if no languages detected
"""
def detect_language_frameworks(self, project_root: Path, language: str) -> List[str]:
"""
Detect frameworks for a specific language.
Args:
project_root: Path to project root
language: Language identifier (e.g., "python", "typescript")
Returns:
List of framework names (e.g., ["django", "flask"])
Example:
>>> detector.detect_language_frameworks(Path("/app"), "python")
["django"]
"""
__all__ = ["LanguageDetector", "LanguageDetection"]Dependencies: Standard library only (pathlib, collections)
Testing Strategy:
- Unit tests with mock file systems (60%)
- Integration tests with fixture projects (30%)
- End-to-end tests with real projects (10%)
2. lsp_configurator.py
Purpose: Generate LSP server configuration for detected languages
Public API:
from typing import Dict, Optional
from dataclasses import dataclass
from pathlib import Path
@dataclass
class LSPServerConfig:
"""Configuration for a single LSP server (all three layers)"""
language: str # Language identifier
server_name: str # LSP server name (e.g., "pyright", "vtsls")
# Layer 1: System binary
layer1_path: Optional[Path] # Path to system LSP binary (None if not installed)
layer1_install_cmd: str # Command to install system binary
layer1_status: str # "installed", "not_found", "error"
# Layer 2: Claude Code plugin
layer2_installed: bool # True if Claude Code plugin installed
layer2_install_cmd: str # Command to install plugin (npx cclsp install ...)
layer2_status: str # "installed", "not_found", "error"
# Layer 3: Project configuration
config: Dict[str, any] # Project-specific settings for .env
overall_status: str # "ready", "partial", "not_configured"
class LSPConfigurator:
"""Generates LSP server configurations"""
def configure_language(
self,
language: str,
project_root: Path,
detection: LanguageDetection
) -> LSPServerConfig:
"""
Generate LSP configuration for a specific language.
Args:
language: Language identifier
project_root: Path to project root
detection: LanguageDetection result from language_detector
Returns:
LSPServerConfig with server path and configuration
Example:
>>> configurator = LSPConfigurator()
>>> config = configurator.configure_language(
... "python",
... Path("/project"),
... detection
... )
>>> print(config.server_name)
pyright
>>> print(config.status)
installed
"""
def check_server_installed(self, server_name: str) -> Optional[Path]:
"""
Check if LSP server is installed on system.
Args:
server_name: Name of LSP server (e.g., "pyright")
Returns:
Path to server executable if found, None otherwise
Example:
>>> configurator.check_server_installed("pyright")
PosixPath('/usr/local/bin/pyright')
"""
def generate_env_config(
self,
configs: List[LSPServerConfig],
project_root: Path
) -> Dict[str, str]:
"""
Generate .env configuration entries for LSP servers (Layer 3).
Args:
configs: List of LSP server configurations
project_root: Path to project root
Returns:
Dictionary of environment variable key-value pairs
Example:
>>> env_config = configurator.generate_env_config(configs, root)
>>> print(env_config)
{
'ENABLE_LSP_TOOL': '1', # REQUIRED
'LSP_PYTHON_INTERPRETER': '/path/.venv/bin/python',
'LSP_PYRIGHT_PATH': '/usr/local/bin/pyright',
'LSP_VTSLS_PATH': '/usr/local/bin/vtsls'
}
Note:
ENABLE_LSP_TOOL=1 is automatically included and is required for LSP
features to activate in Claude Code.
"""
def detect_language_specific_config(
self,
language: str,
project_root: Path
) -> Dict[str, str]:
"""
Detect language-specific configuration (virtual envs, project roots).
Args:
language: Language identifier
project_root: Path to project root
Returns:
Dictionary of language-specific configuration
Example:
>>> configurator.detect_language_specific_config("python", root)
{
'python_interpreter': '/path/.venv/bin/python',
'venv_path': '/path/.venv'
}
"""
__all__ = ["LSPConfigurator", "LSPServerConfig"]Dependencies: Standard library only (pathlib, shutil, subprocess)
Testing Strategy:
- Unit tests with mocked system commands (60%)
- Integration tests with real LSP server checks (30%)
- End-to-end tests with full configuration generation (10%)
3. plugin_manager.py
Purpose: Manage LSP server lifecycle and Claude Code plugin integration
Public API:
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class PluginStatus:
"""Status of a Claude Code LSP plugin"""
language: str
server_name: str
enabled: bool
connected: bool
error: Optional[str] = None
class PluginManager:
"""Manages Claude Code LSP plugin lifecycle"""
def install_plugin(self, config: LSPServerConfig) -> bool:
"""
Install LSP server plugin in Claude Code.
Args:
config: LSP server configuration
Returns:
True if installation successful, False otherwise
Example:
>>> manager = PluginManager()
>>> success = manager.install_plugin(config)
>>> print(success)
True
"""
def verify_connection(self, server_name: str) -> PluginStatus:
"""
Verify LSP server is connected and responding.
Args:
server_name: Name of LSP server
Returns:
PluginStatus with connection details
Example:
>>> status = manager.verify_connection("pyright")
>>> print(f"{status.server_name}: {'Connected' if status.connected else 'Disconnected'}")
pyright: Connected
"""
def get_all_plugin_status(self) -> List[PluginStatus]:
"""
Get status of all installed LSP plugins.
Returns:
List of PluginStatus for all plugins
Example:
>>> statuses = manager.get_all_plugin_status()
>>> for status in statuses:
... print(f"{status.language}: {status.server_name} - {status.connected}")
python: pyright - True
typescript: typescript-language-server - True
"""
def restart_plugin(self, server_name: str) -> bool:
"""
Restart an LSP server plugin.
Args:
server_name: Name of LSP server to restart
Returns:
True if restart successful, False otherwise
"""
__all__ = ["PluginManager", "PluginStatus"]Dependencies: Claude Code SDK (LSP management APIs)
Testing Strategy:
- Unit tests with mocked Claude Code SDK (60%)
- Integration tests with test LSP servers (30%)
- End-to-end tests with real Claude Code instance (10%)
4. status_tracker.py
Purpose: Track configuration status and provide user feedback
Public API:
from typing import List, Dict
from enum import Enum
from dataclasses import dataclass
class ConfigStatus(Enum):
"""Status of LSP configuration"""
NOT_STARTED = "not_started"
DETECTING = "detecting"
CONFIGURING = "configuring"
VERIFYING = "verifying"
COMPLETE = "complete"
ERROR = "error"
@dataclass
class StatusReport:
"""Comprehensive status report"""
overall_status: ConfigStatus
languages_detected: List[LanguageDetection]
servers_configured: List[LSPServerConfig]
plugins_installed: List[PluginStatus]
errors: List[str]
warnings: List[str]
class StatusTracker:
"""Tracks and reports LSP configuration status"""
def update_status(self, status: ConfigStatus, message: str) -> None:
"""
Update current configuration status.
Args:
status: New configuration status
message: Status message for user
Example:
>>> tracker = StatusTracker()
>>> tracker.update_status(ConfigStatus.DETECTING, "Scanning for languages...")
"""
def add_success(self, component: str, message: str) -> None:
"""Record successful operation"""
def add_warning(self, component: str, message: str) -> None:
"""Record warning (non-fatal issue)"""
def add_error(self, component: str, message: str) -> None:
"""Record error (fatal issue)"""
def get_report(self) -> StatusReport:
"""
Generate comprehensive status report.
Returns:
StatusReport with all configuration details
Example:
>>> report = tracker.get_report()
>>> print(f"Status: {report.overall_status}")
>>> print(f"Languages: {len(report.languages_detected)}")
Status: complete
Languages: 3
"""
def format_report(self, report: StatusReport) -> str:
"""
Format status report for display to user.
Args:
report: StatusReport to format
Returns:
Formatted string for terminal output
Example:
>>> output = tracker.format_report(report)
>>> print(output)
[LSP Setup] Configuration complete! 3/3 servers ready.
"""
__all__ = ["StatusTracker", "StatusReport", "ConfigStatus"]Dependencies: Standard library only (enum, dataclasses)
Testing Strategy:
- Unit tests for status tracking logic (60%)
- Integration tests for report generation (30%)
- End-to-end tests for formatted output (10%)
Configuration Files
language_definitions.json
Defines supported languages and their LSP servers:
{
"languages": {
"python": {
"extensions": [".py", ".pyi"],
"lsp_server": "pyright",
"layer1_install": "npm install -g pyright",
"layer2_install": "npx cclsp install pyright",
"framework_markers": {
"django": ["manage.py", "settings.py"],
"flask": ["app.py", "requirements.txt"],
"fastapi": ["main.py", "requirements.txt"]
},
"config_detection": {
"venv": [".venv", "venv", ".virtualenv"],
"requirements": ["requirements.txt", "pyproject.toml", "setup.py"]
}
},
"typescript": {
"extensions": [".ts", ".tsx"],
"lsp_server": "vtsls",
"layer1_install": "npm install -g @vtsls/language-server",
"layer2_install": "npx cclsp install vtsls",
"framework_markers": {
"react": ["package.json:react"],
"vue": ["package.json:vue"],
"angular": ["angular.json"]
},
"config_detection": {
"tsconfig": ["tsconfig.json"],
"node_modules": ["node_modules"]
}
},
"ruby": {
"extensions": [".rb"],
"lsp_server": "ruby-lsp",
"layer1_install": "gem install ruby-lsp",
"layer2_install": "npx cclsp install ruby-lsp"
},
"php": {
"extensions": [".php"],
"lsp_server": "phpactor",
"layer1_install": "composer global require phpactor/phpactor",
"layer2_install": "npx cclsp install phpactor"
}
}
}Note: Each language definition now includes both layer1_install (system binary) and layer2_install (Claude Code plugin) commands.
lsp_server_templates.json
Configuration templates for LSP servers:
{
"pyright": {
"initialization_options": {
"python": {
"analysis": {
"typeCheckingMode": "basic",
"autoSearchPaths": true,
"useLibraryCodeForTypes": true
}
}
},
"settings": {
"python.analysis.diagnosticMode": "workspace"
}
},
"vtsls": {
"initialization_options": {
"preferences": {
"includeInlayParameterNameHints": "all",
"includeInlayFunctionParameterTypeHints": true
}
}
},
"ruby-lsp": {
"initialization_options": {
"enabledFeatures": ["diagnostics", "formatting", "codeActions"]
}
},
"phpactor": {
"initialization_options": {
"language_server_phpstan.enabled": true,
"language_server_psalm.enabled": false
}
}
}Public API (Module Level)
The module exports a clean public API through __init__.py:
"""LSP Auto-Configuration Module
Automatically detects languages and configures LSP servers for Claude Code.
Philosophy:
- Ruthless simplicity: No complex abstractions
- Self-contained: Standard library only (except Claude Code SDK)
- User-guided: No automatic system binary installation
- Regeneratable: Clear specifications enable AI rebuilding
Public API (the "studs"):
LanguageDetector: Detect languages in project
LSPConfigurator: Generate LSP configuration
PluginManager: Manage Claude Code LSP plugins
StatusTracker: Track and report configuration status
configure_project: High-level function for full project setup
Usage:
>>> from lsp_setup import configure_project
>>> result = configure_project(Path("/path/to/project"))
>>> print(result.format_report())
"""
from .language_detector import LanguageDetector, LanguageDetection
from .lsp_configurator import LSPConfigurator, LSPServerConfig
from .plugin_manager import PluginManager, PluginStatus
from .status_tracker import StatusTracker, StatusReport, ConfigStatus
__all__ = [
"LanguageDetector",
"LanguageDetection",
"LSPConfigurator",
"LSPServerConfig",
"PluginManager",
"PluginStatus",
"StatusTracker",
"StatusReport",
"ConfigStatus",
"configure_project",
]
def configure_project(
project_root: Path,
languages: Optional[List[str]] = None,
force: bool = False,
status_only: bool = False
) -> StatusReport:
"""
High-level function to configure LSP for entire project.
Args:
project_root: Path to project root directory
languages: Optional list of specific languages to configure
force: Force reconfiguration even if already configured
status_only: Only check status, don't modify configuration
Returns:
StatusReport with configuration results
Example:
>>> from pathlib import Path
>>> from lsp_setup import configure_project
>>> result = configure_project(Path.cwd())
>>> print(result.format_report())
[LSP Setup] Configuration complete! 3/3 servers ready.
"""Testing Strategy
Test Coverage Requirements
- Unit Tests (60%): Fast, heavily mocked
- Integration Tests (30%): Multiple components, fixture projects
- End-to-End Tests (10%): Complete workflows, real LSP servers
Test Execution
# Run all tests
pytest tests/
# Run specific test module
pytest tests/test_language_detector.py
# Run with coverage
pytest --cov=lsp_setup --cov-report=html tests/
# Run only fast tests (unit)
pytest -m unit tests/
# Run integration tests
pytest -m integration tests/Test Fixtures
Located in tests/fixtures/:
sample_python_project/: Django project with virtual environmentsample_typescript_project/: React project with TypeScriptsample_polyglot_project/: Mixed Python/TypeScript/Rust projectsample_empty_project/: Empty directory for edge case testing
Testing Philosophy
Follow TDD Pyramid:
- Most tests are unit tests (fast, focused)
- Strategic integration tests for component interaction
- Minimal E2E tests for critical user workflows
Zero-BS Testing:
- No tests for non-existent features
- Every test validates real behavior
- Mock external dependencies (LSP servers, file system) strategically
- Use real file I/O for integration tests
Contributing
Adding New Language Support
1. Update config/language_definitions.json with language details:
{
"new_language": {
"extensions": [".ext"],
"lsp_server": "language-server-name",
"install_command": "install command here",
"framework_markers": {},
"config_detection": {}
}
}2. Add LSP server template to config/lsp_server_templates.json
3. Create test fixture in tests/fixtures/sample_new_language_project/
4. Add tests in tests/test_language_detector.py
5. Update SKILL.md supported languages table
Code Style
- Follow PEP 8 for Python code
- Use type hints for all public APIs
- Document all public functions with docstrings
- Keep functions under 50 lines (ruthless simplicity)
- Prefer explicit over implicit
Pull Request Checklist
- [ ] All tests pass (
pytest tests/) - [ ] Test coverage ≥ 80% (
pytest --cov) - [ ] Updated documentation (SKILL.md, README.md, USAGE_EXAMPLES.md)
- [ ] Added type hints to new functions
- [ ] Followed brick philosophy (self-contained, clear API)
- [ ] No stub functions or placeholders
- [ ] Updated
__all__exports if public API changed
Maintenance
Version Updates
When LSP servers change their APIs or installation methods:
1. Update config/language_definitions.json with new install commands 2. Update config/lsp_server_templates.json with new configuration options 3. Add migration guide in SKILL.md troubleshooting section 4. Update tests to reflect API changes
Deprecation Process
When removing support for a language or LSP server:
1. Add deprecation warning in skill output (1 release) 2. Document removal timeline in SKILL.md 3. Remove from language_definitions.json (next major release) 4. Archive tests in tests/archived/
Performance Characteristics
- Language Detection: O(n) where n = number of files in project
- Configuration Generation: O(m) where m = number of languages detected
- Plugin Installation: O(p) where p = number of plugins (sequential)
- Verification: O(p) where p = number of plugins (parallel)
Typical Execution Times:
- Small project (< 100 files): 2-3 seconds
- Medium project (100-1000 files): 3-5 seconds
- Large project (> 1000 files): 5-10 seconds
Security Considerations
Safe Subprocess Execution:
- Never execute arbitrary shell commands from user input
- Validate LSP server paths before execution
- Use absolute paths for all subprocess calls
- Timeout all subprocess operations (30 second default)
File System Safety:
- Never overwrite
.envwithout user confirmation (unless--force) - Validate all file paths are within project root
- Handle symbolic links safely (resolve before checking)
Network Safety:
- No automatic download of LSP servers
- All installation commands are user-executed
- No telemetry or external API calls
Dependencies
Required:
- Python 3.9+
- Claude Code SDK (LSP management APIs)
Optional:
- None (all LSP servers are external to the module)
License
Same as amplihack project (see root LICENSE file).
Support
For issues or questions:
1. Check troubleshooting section in SKILL.md 2. Review usage examples in USAGE_EXAMPLES.md 3. Open issue in amplihack repository with:
- Project structure (language breakdown)
- LSP server versions
- Output of
/lsp-setup --status-only - Relevant error messages
"""
Status Tracker for LSP three-layer architecture.
Philosophy:
- Checks all three layers: System binaries, Claude plugins, .env config
- Generates actionable user guidance for missing components
- Platform-specific installation instructions
- Zero-BS: Real checks, no fake status
Public API:
StatusTracker: Tracks LSP configuration status across three layers
check_layer_1: Check system LSP binaries
check_layer_2: Check Claude Code plugins
check_layer_3: Check .env configuration
get_full_status: Get complete status across all layers
generate_user_guidance: Create actionable setup instructions
"""
import platform
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Any
__all__ = ["StatusTracker", "LayerStatus"]
@dataclass
class LayerStatus:
"""Status for a single layer."""
installed: bool
details: str | None = None
install_guide: str | None = None
class StatusTracker:
"""Tracks LSP configuration status across three layers."""
# Mapping of languages to system binaries
LANGUAGE_TO_BINARY = {
"python": "pyright",
"typescript": "vtsls",
"javascript": "vtsls",
"rust": "rust-analyzer",
"go": "gopls",
"java": "jdtls",
"cpp": "clangd",
"c": "clangd",
"ruby": "ruby-lsp",
"php": "phpactor",
"csharp": "omnisharp",
"kotlin": "kotlin-language-server",
"swift": "sourcekit-lsp",
"scala": "metals",
"lua": "lua-language-server",
"elixir": "elixir-ls",
"haskell": "haskell-language-server",
}
# Platform-specific install commands
INSTALL_COMMANDS = {
"python": {
"darwin": "npm install -g pyright",
"linux": "npm install -g pyright",
},
"typescript": {
"darwin": "npm install -g @vtsls/language-server",
"linux": "npm install -g @vtsls/language-server",
},
"javascript": {
"darwin": "npm install -g @vtsls/language-server",
"linux": "npm install -g @vtsls/language-server",
},
"rust": {
"darwin": "rustup component add rust-analyzer",
"linux": "rustup component add rust-analyzer",
},
"go": {
"darwin": "go install golang.org/x/tools/gopls@latest",
"linux": "go install golang.org/x/tools/gopls@latest",
},
"java": {
"darwin": "brew install jdtls",
"linux": "Download from eclipse.org/jdtls",
},
"cpp": {
"darwin": "brew install llvm",
"linux": "sudo apt install clangd",
},
"c": {
"darwin": "brew install llvm",
"linux": "sudo apt install clangd",
},
"ruby": {
"darwin": "gem install ruby-lsp",
"linux": "gem install ruby-lsp",
},
"php": {
"darwin": "composer global require phpactor/phpactor",
"linux": "composer global require phpactor/phpactor",
},
}
def __init__(self, project_root: Path, languages: list[str]):
"""
Initialize status tracker.
Args:
project_root: Path to project root directory
languages: List of language names to track
"""
self.project_root = Path(project_root)
self.languages = languages
self.env_file = self.project_root / ".env"
def check_layer_1(self) -> dict[str, dict]:
"""
Check Layer 1: System LSP binaries.
Returns:
Dict mapping language -> status info
"""
status = {}
for lang in self.languages:
binary = self.LANGUAGE_TO_BINARY.get(lang)
if not binary:
status[lang] = {"installed": False, "error": f"Unknown language: {lang}"}
continue
# Check if binary exists
binary_path = shutil.which(binary)
is_installed = binary_path is not None
status[lang] = {
"installed": is_installed,
"binary": binary,
"path": binary_path if is_installed else None,
}
# Add install guide if missing
if not is_installed:
status[lang]["install_guide"] = self._get_install_command(lang)
return status
def check_layer_2(self) -> dict[str, dict]:
"""
Check Layer 2: Claude Code plugins.
Returns:
Dict mapping language -> plugin status
"""
from .plugin_manager import PluginManager
manager = PluginManager()
installed_plugins = manager.list_installed_plugins()
status = {}
for lang in self.languages:
is_installed = lang in installed_plugins
status[lang] = {
"installed": is_installed,
"plugin_name": lang,
}
if not is_installed:
status[lang]["install_guide"] = f"npx cclsp install {lang}"
return status
def check_layer_3(self) -> dict[str, Any]:
"""
Check Layer 3: .env configuration.
Returns:
Dict with enabled status and config details
"""
if not self.env_file.exists():
return {
"enabled": False,
"env_file_exists": False,
"install_guide": "Run: echo 'ENABLE_LSP_TOOL=1' >> .env",
}
# Check if ENABLE_LSP_TOOL=1 is set
content = self.env_file.read_text()
enabled = "ENABLE_LSP_TOOL=1" in content
return {
"enabled": enabled,
"env_file_exists": True,
"path": str(self.env_file),
"install_guide": "Add 'ENABLE_LSP_TOOL=1' to .env file" if not enabled else None,
}
def get_full_status(self) -> dict[str, Any]:
"""
Get complete status across all three layers.
Returns:
Dict with status for all layers plus overall readiness
"""
layer_1 = self.check_layer_1()
layer_2 = self.check_layer_2()
layer_3 = self.check_layer_3()
# Check if all layers are ready
all_layer_1_ready = all(lang_status["installed"] for lang_status in layer_1.values())
all_layer_2_ready = all(lang_status["installed"] for lang_status in layer_2.values())
layer_3_ready = layer_3["enabled"]
overall_ready = all_layer_1_ready and all_layer_2_ready and layer_3_ready
return {
"layer_1": layer_1,
"layer_2": layer_2,
"layer_3": layer_3,
"overall_ready": overall_ready,
}
def generate_user_guidance(self) -> str:
"""
Generate actionable setup guidance based on current status.
Returns:
Formatted string with setup instructions
"""
status = self.get_full_status()
if status["overall_ready"]:
return "✅ All LSP layers configured! No action needed."
guidance_parts = ["LSP Setup Status:\n"]
# Layer 1 guidance
layer_1_issues = [
(lang, info) for lang, info in status["layer_1"].items() if not info["installed"]
]
if layer_1_issues:
guidance_parts.append("\n🔴 Layer 1: Missing System LSP Binaries")
for lang, info in layer_1_issues:
guidance_parts.append(f" • {lang}: {info.get('install_guide', 'See docs')}")
# Layer 2 guidance
layer_2_issues = [
(lang, info) for lang, info in status["layer_2"].items() if not info["installed"]
]
if layer_2_issues:
guidance_parts.append("\n🟡 Layer 2: Missing Claude Code Plugins")
for lang, info in layer_2_issues:
guidance_parts.append(f" • {lang}: {info.get('install_guide', 'See docs')}")
# Layer 3 guidance
if not status["layer_3"]["enabled"]:
guidance_parts.append("\n🟠 Layer 3: .env Configuration Missing")
guidance_parts.append(f" • {status['layer_3'].get('install_guide', 'See docs')}")
return "\n".join(guidance_parts)
def _get_install_command(self, language: str) -> str:
"""Get platform-specific install command for a language."""
system = platform.system().lower()
if system == "darwin":
platform_key = "darwin"
else:
platform_key = "linux"
commands = self.INSTALL_COMMANDS.get(language, {})
return commands.get(platform_key, f"Install {language} LSP server manually")
def get_setup_progress(self) -> dict[str, Any]:
"""
Get setup progress metrics.
Returns:
Dict with progress percentages for each layer
"""
status = self.get_full_status()
total_langs = len(self.languages)
if total_langs == 0:
return {
"layer_1_progress": 100,
"layer_2_progress": 100,
"layer_3_progress": 100,
"overall_progress": 100,
}
# Layer 1 progress
layer_1_installed = sum(1 for info in status["layer_1"].values() if info["installed"])
layer_1_progress = (layer_1_installed / total_langs) * 100
# Layer 2 progress
layer_2_installed = sum(1 for info in status["layer_2"].values() if info["installed"])
layer_2_progress = (layer_2_installed / total_langs) * 100
# Layer 3 progress (binary: 0 or 100)
layer_3_progress = 100 if status["layer_3"]["enabled"] else 0
# Overall progress (weighted average)
overall_progress = layer_1_progress * 0.4 + layer_2_progress * 0.4 + layer_3_progress * 0.2
return {
"layer_1_progress": round(layer_1_progress, 1),
"layer_2_progress": round(layer_2_progress, 1),
"layer_3_progress": round(layer_3_progress, 1),
"overall_progress": round(overall_progress, 1),
}
def get_next_action(self) -> str | None:
"""Get the next recommended action for setup."""
status = self.get_full_status()
if status["overall_ready"]:
return None
# Priority: Layer 3 -> Layer 1 -> Layer 2
if not status["layer_3"]["enabled"]:
return status["layer_3"].get("install_guide", "Configure .env")
for lang, info in status["layer_1"].items():
if not info["installed"]:
return info.get("install_guide", f"Install {lang} LSP binary")
for lang, info in status["layer_2"].items():
if not info["installed"]:
return info.get("install_guide", f"Install {lang} plugin")
return None
def get_missing_components(self) -> list[str]:
"""Get list of missing components."""
status = self.get_full_status()
missing = []
for lang, info in status["layer_1"].items():
if not info["installed"]:
missing.append(f"Layer 1: {lang} binary")
for lang, info in status["layer_2"].items():
if not info["installed"]:
missing.append(f"Layer 2: {lang} plugin")
if not status["layer_3"]["enabled"]:
missing.append("Layer 3: .env configuration")
return missing
def get_completion_percentage(self) -> float:
"""Get overall completion percentage."""
progress = self.get_setup_progress()
return progress["overall_progress"]
def validate_layer_dependencies(self) -> dict[str, bool]:
"""Validate dependencies between layers."""
status = self.get_full_status()
return {
"layer_2_requires_layer_1": True, # Plugins need binaries
"layer_3_independent": True, # .env is independent
}
def export_status_report(self, format: str = "text") -> str:
"""Export status report in specified format."""
if format == "json":
import json
return json.dumps(self.get_full_status(), indent=2)
return self.generate_user_guidance()
def get_troubleshooting_tips(self) -> list[str]:
"""Get troubleshooting tips based on current status."""
status = self.get_full_status()
tips = []
if not status["overall_ready"]:
tips.append("Check that npx is installed: npx --version")
tips.append("Verify PATH includes LSP binaries: echo $PATH")
tips.append("Check .env file syntax")
return tips
def get_platform_requirements(self) -> dict[str, list[str]]:
"""Get platform-specific requirements."""
system = platform.system().lower()
platform_name = "darwin" if system == "darwin" else "linux"
requirements = {}
for lang in self.languages:
cmd = self._get_install_command(lang)
requirements[lang] = [cmd]
return requirements
def get_install_commands(self) -> dict[str, str]:
"""Get install commands for all languages."""
commands = {}
for lang in self.languages:
commands[lang] = self._get_install_command(lang)
return commands
def estimate_setup_time(self) -> int:
"""Estimate setup time in minutes."""
status = self.get_full_status()
missing_count = len(self.get_missing_components())
# Rough estimate: 2 minutes per missing component
return missing_count * 2
"""
Test suite for LSP Auto-Configuration system.
Testing pyramid:
- 60% Unit tests (fast, heavily mocked)
- 30% Integration tests (multiple components)
- 10% E2E tests (complete workflows)
Target ratio: 3:1 to 5:1 (test lines : implementation lines)
"""
__all__ = []
"""
Pytest fixtures for LSP Auto-Configuration tests.
Provides common test fixtures and utilities following TDD principles.
"""
from pathlib import Path
from unittest.mock import MagicMock, Mock
import pytest
@pytest.fixture
def mock_project_root(tmp_path: Path) -> Path:
"""Create a temporary project root directory."""
return tmp_path
@pytest.fixture
def mock_env_file(mock_project_root: Path) -> Path:
"""Create a mock .env file path."""
env_file = mock_project_root / ".env"
return env_file
@pytest.fixture
def sample_python_files(mock_project_root: Path) -> list[Path]:
"""Create sample Python files for language detection."""
files = []
for name in ["main.py", "utils.py", "tests/test_main.py"]:
file_path = mock_project_root / name
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("# Python code")
files.append(file_path)
return files
@pytest.fixture
def sample_typescript_files(mock_project_root: Path) -> list[Path]:
"""Create sample TypeScript files for language detection."""
files = []
for name in ["index.ts", "utils.ts", "tests/test.ts"]:
file_path = mock_project_root / name
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("// TypeScript code")
files.append(file_path)
return files
@pytest.fixture
def sample_mixed_language_files(mock_project_root: Path) -> dict[str, list[Path]]:
"""Create a project with multiple languages."""
files = {
"python": [],
"typescript": [],
"javascript": [],
"rust": [],
}
# Python files
for name in ["main.py", "utils.py"]:
file_path = mock_project_root / name
file_path.write_text("# Python")
files["python"].append(file_path)
# TypeScript files
for name in ["index.ts", "types.ts"]:
file_path = mock_project_root / name
file_path.write_text("// TypeScript")
files["typescript"].append(file_path)
# JavaScript files
for name in ["app.js", "config.js"]:
file_path = mock_project_root / name
file_path.write_text("// JavaScript")
files["javascript"].append(file_path)
# Rust files
for name in ["main.rs", "lib.rs"]:
file_path = mock_project_root / "src" / name
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("// Rust")
files["rust"].append(file_path)
return files
@pytest.fixture
def mock_subprocess_run():
"""Mock subprocess.run for external command testing."""
mock = MagicMock()
mock.return_value.returncode = 0
mock.return_value.stdout = ""
mock.return_value.stderr = ""
return mock
@pytest.fixture
def mock_shutil_which():
"""Mock shutil.which for binary detection."""
return MagicMock()
@pytest.fixture
def installed_lsp_binaries() -> dict[str, str]:
"""Simulate installed LSP binaries."""
return {
"pyright": "/usr/local/bin/pyright",
"typescript-language-server": "/usr/local/bin/typescript-language-server",
"rust-analyzer": "/usr/local/bin/rust-analyzer",
}
@pytest.fixture
def missing_lsp_binaries() -> dict[str, None]:
"""Simulate missing LSP binaries."""
return {
"pyright": None,
"typescript-language-server": None,
"rust-analyzer": None,
}
@pytest.fixture
def mock_npx_cclsp_success(mock_subprocess_run):
"""Mock successful npx cclsp install."""
def mock_run(cmd, *args, **kwargs):
result = Mock()
if "cclsp" in cmd and "list" in cmd:
result.returncode = 0
result.stdout = "python\ntypescript\nrust\n"
result.stderr = ""
elif "cclsp" in cmd and "install" in cmd:
result.returncode = 0
result.stdout = "Successfully installed plugin"
result.stderr = ""
else:
result.returncode = 0
result.stdout = ""
result.stderr = ""
return result
mock_subprocess_run.side_effect = mock_run
return mock_subprocess_run
@pytest.fixture
def mock_npx_cclsp_failure(mock_subprocess_run):
"""Mock failed npx cclsp install."""
def mock_run(cmd, *args, **kwargs):
result = Mock()
if "cclsp" in cmd:
result.returncode = 1
result.stdout = ""
result.stderr = "Failed to install plugin"
else:
result.returncode = 0
result.stdout = ""
result.stderr = ""
return result
mock_subprocess_run.side_effect = mock_run
return mock_subprocess_run
@pytest.fixture
def language_to_lsp_mapping() -> dict[str, dict[str, str]]:
"""Standard mapping of languages to LSP servers."""
return {
"python": {
"binary": "pyright",
"plugin": "python",
"install_guide": "npm install -g pyright",
},
"typescript": {
"binary": "typescript-language-server",
"plugin": "typescript",
"install_guide": "npm install -g typescript-language-server",
},
"javascript": {
"binary": "typescript-language-server",
"plugin": "typescript",
"install_guide": "npm install -g typescript-language-server",
},
"rust": {
"binary": "rust-analyzer",
"plugin": "rust",
"install_guide": "rustup component add rust-analyzer",
},
"go": {
"binary": "gopls",
"plugin": "go",
"install_guide": "go install golang.org/x/tools/gopls@latest",
},
}
@pytest.fixture
def mock_platform_system():
"""Mock platform.system() for platform-specific tests."""
return MagicMock(return_value="Darwin") # Default to macOS
""" LSP Auto-Configuration Test Suite ==================================
Comprehensive TDD test suite following the Testing Pyramid principle.
Test Structure
tests/
├── conftest.py # Pytest fixtures and test utilities
├── test_language_detector.py # Unit tests (60%)
├── test_lsp_configurator.py # Unit tests (60%)
├── test_plugin_manager.py # Unit tests (60%)
├── test_status_tracker.py # Unit tests (60%)
├── test_integration.py # Integration tests (30%)
└── test_e2e.py # End-to-end tests (10%)Testing Pyramid
60% Unit Tests (Fast, Heavily Mocked)
- test_language_detector.py: 33 tests
- Language detection for all 16 languages
- File extension mapping
- Directory exclusion (.gitignore, node_modules)
- Confidence scoring
- test_lsp_configurator.py: 22 tests
- .env file creation and modification
- LSP enable/disable functionality
- Environment variable management
- Error handling (permissions, I/O)
- Comment/whitespace preservation
- test_plugin_manager.py: 25 tests
- Plugin installation via
npx cclsp - Plugin listing and status checks
- Retry logic and timeout handling
- npx availability detection
- Installation command generation
- test_status_tracker.py: 26 tests
- Three-layer status checking
- User guidance generation
- Platform-specific requirements
- Troubleshooting tips
- Progress tracking
Total Unit Tests: 106 tests
30% Integration Tests (Multiple Components)
- test_integration.py: 15 tests
- Language detection + status checking
- Plugin installation workflows
- Multi-language setup
- Error handling across modules
- Status reporting
10% E2E Tests (Complete Workflows)
- test_e2e.py: 18 tests
- First-time user auto-setup
- Manual setup with guidance
- Multi-language projects
- Troubleshooting workflows
- Platform-specific scenarios
- Edge cases (empty project, large project)
Total Tests: 139 comprehensive tests
Running Tests
Run All Tests
pytest .claude/skills/lsp-setup/tests/Run by Category
# Unit tests only (fast)
pytest .claude/skills/lsp-setup/tests/test_language_detector.py
pytest .claude/skills/lsp-setup/tests/test_lsp_configurator.py
pytest .claude/skills/lsp-setup/tests/test_plugin_manager.py
pytest .claude/skills/lsp-setup/tests/test_status_tracker.py
# Integration tests
pytest .claude/skills/lsp-setup/tests/test_integration.py
# E2E tests
pytest .claude/skills/lsp-setup/tests/test_e2e.pyRun with Coverage
pytest --cov=lsp_setup --cov-report=html .claude/skills/lsp-setup/tests/Run Specific Test
pytest .claude/skills/lsp-setup/tests/test_language_detector.py::TestLanguageDetector::test_detect_single_python_projectTest Fixtures (conftest.py)
Directory Fixtures
mock_project_root: Temporary project directorymock_env_file: Mock .env file path
File Fixtures
sample_python_files: Python project structuresample_typescript_files: TypeScript project structuresample_mixed_language_files: Multi-language project
Mock Fixtures
mock_subprocess_run: Mock subprocess callsmock_shutil_which: Mock binary detectionmock_npx_cclsp_success: Mock successful plugin installationmock_npx_cclsp_failure: Mock failed plugin installation
Data Fixtures
installed_lsp_binaries: Simulated installed LSP serversmissing_lsp_binaries: Simulated missing LSP serverslanguage_to_lsp_mapping: Language to LSP server mapping
Test Coverage Goals
Module Coverage Targets
language_detector.py: 95%+ coveragelsp_configurator.py: 90%+ coverageplugin_manager.py: 90%+ coveragestatus_tracker.py: 95%+ coverage
Critical Paths (Must be 100%)
- Language detection for all 16 languages
- Three-layer status checking
- .env file manipulation
- Plugin installation via npx cclsp
TDD Red-Green-Refactor Cycle
Current State: RED ❌
All tests currently FAIL because implementation doesn't exist yet.
Next Steps (GREEN ✅)
1. Implement language_detector.py to pass unit tests 2. Implement lsp_configurator.py to pass unit tests 3. Implement plugin_manager.py to pass unit tests 4. Implement status_tracker.py to pass unit tests 5. Verify integration tests pass 6. Verify E2E tests pass
Refactor Phase
Once all tests pass:
1. Identify code duplication 2. Extract common patterns 3. Optimize performance 4. Improve error messages 5. Re-run all tests to ensure refactoring didn't break anything
Test Ratio Analysis
Current Test-to-Code Ratio
- 139 comprehensive tests written
- ~350 lines per test file (average)
- Total test code: ~2,100 lines
- Expected implementation: ~500-700 lines
- Ratio: 3:1 to 4:1 (within target 3:1 to 5:1)
This ratio ensures:
- Comprehensive coverage without over-testing
- Fast test execution (unit tests run in seconds)
- Clear test intent (each test tests ONE thing)
- Maintainable test suite (not excessive)
Key Testing Patterns Used
1. Arrange-Act-Assert
Every test follows AAA pattern:
def test_example(self, mock_project_root):
# Arrange
(mock_project_root / "test.py").write_text("# Python")
# Act
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
# Assert
assert "python" in languages2. Strategic Mocking
Mock external dependencies (filesystem, subprocess, network):
with patch("subprocess.run", mock_subprocess_run):
manager = PluginManager()
result = manager.install_plugin("python")3. Parametrized Tests (Not Used Here)
Could be added for testing all 16 languages:
@pytest.mark.parametrize("language,extension", [
("python", ".py"),
("typescript", ".ts"),
("rust", ".rs"),
])
def test_language_detection(language, extension):
...Dependencies
pip install pytest pytest-cov pytest-mockCI Integration
GitHub Actions Example
- name: Run Tests
run: |
pytest .claude/skills/lsp-setup/tests/ \
--cov=lsp_setup \
--cov-report=xml \
--cov-fail-under=90Troubleshooting
Import Errors
If tests fail with import errors:
# Ensure lsp_setup package is in PYTHONPATH
export PYTHONPATH="${PYTHONPATH}:.claude/skills"
pytest .claude/skills/lsp-setup/tests/Fixture Not Found
Ensure conftest.py is in the same directory as test files.
Mock Not Working
Check that patches are applied in correct order (innermost first):
with patch("shutil.which", mock_which):
with patch("subprocess.run", mock_run):
# Test codePhilosophy Compliance
This test suite follows amplihack philosophy:
✅ Ruthless Simplicity: Clear, focused tests ✅ Proportionality: 3:1 to 5:1 ratio (not excessive) ✅ Zero-BS: No stub tests, all tests verify real behavior ✅ Fast Execution: Unit tests run in seconds ✅ Clear Intent: Each test has ONE purpose
Next Steps
1. Run tests: pytest .claude/skills/lsp-setup/tests/ (expect ALL to FAIL) 2. Implement modules: Write code to make tests pass 3. Iterate: Red -> Green -> Refactor 4. Verify coverage: pytest --cov 5. Document learnings: Update DISCOVERIES.md
---
Remember: These tests define the contract. Implementation must satisfy these tests.
"""
End-to-end tests for LSP Auto-Configuration (10% - E2E Tests)
Tests complete user workflows from start to finish.
All tests should FAIL initially (TDD red phase).
"""
from unittest.mock import MagicMock, patch
class TestCompleteSetupWorkflows:
"""E2E tests for complete setup workflows."""
def test_first_time_user_full_auto_setup(
self,
mock_project_root,
sample_python_files,
installed_lsp_binaries,
mock_npx_cclsp_success,
mock_env_file,
):
"""
E2E: First-time user with Python project, LSP binaries installed.
Expected: Full auto-setup completes all 3 layers.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.lsp_configurator import LSPConfigurator
from lsp_setup.plugin_manager import PluginManager
from lsp_setup.status_tracker import StatusTracker
# User starts with Python project, pyright installed
with patch("shutil.which", side_effect=lambda x: installed_lsp_binaries.get(x)):
with patch("subprocess.run", mock_npx_cclsp_success):
# Simulate user running the skill
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
# Auto-setup flow
manager = PluginManager()
manager.install_plugins(languages)
configurator = LSPConfigurator(mock_project_root)
configurator.enable_lsp()
# Verify final state
tracker = StatusTracker(mock_project_root, languages)
status = tracker.get_full_status()
assert status["overall_ready"] is True
assert configurator.is_lsp_enabled() is True
def test_first_time_user_no_binaries_manual_setup(self, mock_project_root, sample_python_files):
"""
E2E: First-time user with Python project, no LSP binaries installed.
Expected: Generate user guidance for manual installation.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.status_tracker import StatusTracker
# User starts with Python project, no binaries
with patch("shutil.which", return_value=None):
with patch("subprocess.run") as mock_run:
mock_run.return_value.stdout = ""
# Simulate user running the skill
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
tracker = StatusTracker(mock_project_root, languages)
guidance = tracker.generate_user_guidance()
# Verify guidance includes all necessary steps
assert "Layer 1" in guidance
assert "pyright" in guidance.lower()
assert "npm install" in guidance.lower() or "pip install" in guidance.lower()
assert "npx cclsp install" in guidance
assert "ENABLE_LSP_TOOL=1" in guidance
def test_multi_language_project_full_setup(
self,
mock_project_root,
sample_mixed_language_files,
language_to_lsp_mapping,
mock_npx_cclsp_success,
mock_env_file,
):
"""
E2E: Multi-language project (Python, TypeScript, Rust, JavaScript).
Expected: Setup all detected languages.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.lsp_configurator import LSPConfigurator
from lsp_setup.plugin_manager import PluginManager
from lsp_setup.status_tracker import StatusTracker
# Mock all binaries installed
def mock_which(binary):
binary_map = {
"pyright": "/usr/local/bin/pyright",
"typescript-language-server": "/usr/local/bin/typescript-language-server",
"rust-analyzer": "/usr/local/bin/rust-analyzer",
}
return binary_map.get(binary)
with patch("shutil.which", side_effect=mock_which):
with patch("subprocess.run", mock_npx_cclsp_success):
# Detect all languages
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
assert len(languages) == 4 # Python, TS, JS, Rust
# Install plugins for all
manager = PluginManager()
results = manager.install_plugins(languages)
for lang in languages:
assert results[lang] is True
# Configure .env
configurator = LSPConfigurator(mock_project_root)
configurator.enable_lsp()
# Verify all languages ready
tracker = StatusTracker(mock_project_root, languages)
status = tracker.get_full_status()
assert status["overall_ready"] is True
def test_existing_user_adding_new_language(
self,
mock_project_root,
sample_python_files,
installed_lsp_binaries,
mock_npx_cclsp_success,
mock_env_file,
):
"""
E2E: Existing user with Python setup, adds TypeScript files.
Expected: Detect new language and extend setup.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.plugin_manager import PluginManager
from lsp_setup.status_tracker import StatusTracker
# Initial setup: Python already configured
mock_env_file.write_text("ENABLE_LSP_TOOL=1\n")
# User adds TypeScript files
(mock_project_root / "index.ts").write_text("// TypeScript")
(mock_project_root / "utils.ts").write_text("// TypeScript")
with patch("shutil.which", side_effect=lambda x: installed_lsp_binaries.get(x)):
with patch("subprocess.run") as mock_run:
# Mock: Python plugin already installed, TS not installed
def mock_run_fn(cmd, *args, **kwargs):
result = MagicMock()
if "list" in cmd:
result.stdout = "python\n" # Only Python installed
elif "install" in cmd and "typescript" in cmd:
result.returncode = 0
result.stdout = "Installed typescript"
else:
result.returncode = 0
result.stdout = ""
return result
mock_run.side_effect = mock_run_fn
# Detect languages (should find Python + TypeScript)
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
assert "python" in languages
assert "typescript" in languages
# Check status
tracker = StatusTracker(mock_project_root, languages)
missing = tracker.get_missing_components()
# TypeScript plugin should be missing
assert "typescript" in missing["plugins"]
# Install TypeScript plugin
manager = PluginManager()
result = manager.install_plugin("typescript")
assert result is True
def test_user_disables_then_re_enables_lsp(
self, mock_project_root, sample_python_files, mock_env_file
):
"""
E2E: User disables LSP, then re-enables it later.
Expected: .env updates correctly both times.
"""
from lsp_setup.lsp_configurator import LSPConfigurator
configurator = LSPConfigurator(mock_project_root)
# Enable LSP
configurator.enable_lsp()
assert configurator.is_lsp_enabled() is True
# Disable LSP
configurator.disable_lsp()
assert configurator.is_lsp_enabled() is False
# Re-enable LSP
configurator.enable_lsp()
assert configurator.is_lsp_enabled() is True
def test_troubleshooting_workflow(
self, mock_project_root, sample_python_files, mock_subprocess_run
):
"""
E2E: User reports LSP not working, troubleshooting flow.
Expected: Identify specific layer that's broken.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.status_tracker import StatusTracker
# Scenario: Layer 1 OK, Layer 2 broken, Layer 3 OK
with patch("shutil.which", return_value="/usr/local/bin/pyright"):
mock_subprocess_run.return_value.stdout = "" # No plugins installed
(mock_project_root / ".env").write_text("ENABLE_LSP_TOOL=1\n")
with patch("subprocess.run", mock_subprocess_run):
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
tracker = StatusTracker(mock_project_root, languages)
issues = tracker.validate_layer_dependencies()
# Should identify Layer 2 as the problem
assert len(issues) > 0
next_action = tracker.get_next_action()
assert "plugin" in next_action.lower()
class TestEdgeCases:
"""E2E tests for edge cases and unusual scenarios."""
def test_empty_project_no_languages(self, mock_project_root):
"""
E2E: Empty project with no source files.
Expected: Graceful handling with appropriate message.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.status_tracker import StatusTracker
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
assert len(languages) == 0
# Status tracker should handle empty language list
tracker = StatusTracker(mock_project_root, languages)
guidance = tracker.generate_user_guidance()
assert "no languages detected" in guidance.lower() or "empty project" in guidance.lower()
def test_unsupported_language_project(self, mock_project_root):
"""
E2E: Project with unsupported language files only.
Expected: Report no supported languages found.
"""
from lsp_setup.language_detector import LanguageDetector
# Create files for unsupported language (e.g., .txt, .md only)
(mock_project_root / "README.md").write_text("# Project")
(mock_project_root / "data.txt").write_text("Some data")
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
assert len(languages) == 0
def test_large_project_with_many_files(self, mock_project_root):
"""
E2E: Large project with 100+ files across multiple languages.
Expected: Efficient detection and setup.
"""
from lsp_setup.language_detector import LanguageDetector
# Create 50 Python files
for i in range(50):
(mock_project_root / f"file{i}.py").write_text("# Python")
# Create 30 TypeScript files
for i in range(30):
(mock_project_root / f"file{i}.ts").write_text("// TypeScript")
# Create 20 JavaScript files
for i in range(20):
(mock_project_root / f"file{i}.js").write_text("// JavaScript")
detector = LanguageDetector(mock_project_root)
languages_with_scores = detector.detect_languages_with_confidence()
assert languages_with_scores["python"] == 50
assert languages_with_scores["typescript"] == 30
assert languages_with_scores["javascript"] == 20
def test_project_with_gitignore_exclusions(self, mock_project_root):
"""
E2E: Project with .gitignore excluding certain directories.
Expected: Respect .gitignore when detecting languages.
"""
from lsp_setup.language_detector import LanguageDetector
# Create .gitignore
(mock_project_root / ".gitignore").write_text("build/\n*.tmp\n")
# Create files (some should be ignored)
(mock_project_root / "main.py").write_text("# Python")
(mock_project_root / "build").mkdir()
(mock_project_root / "build" / "generated.py").write_text("# Should ignore")
(mock_project_root / "temp.tmp").write_text("# Should ignore")
detector = LanguageDetector(mock_project_root)
languages_with_scores = detector.detect_languages_with_confidence()
# Should only count main.py
assert languages_with_scores.get("python", 0) == 1
class TestPlatformSpecific:
"""E2E tests for platform-specific scenarios."""
def test_macos_setup_workflow(
self,
mock_project_root,
sample_python_files,
mock_platform_system,
installed_lsp_binaries,
):
"""
E2E: Setup workflow on macOS.
Expected: macOS-specific installation guidance.
"""
from lsp_setup.status_tracker import StatusTracker
mock_platform_system.return_value = "Darwin"
with patch("platform.system", mock_platform_system):
with patch("shutil.which", return_value=None):
tracker = StatusTracker(mock_project_root, ["python"])
guidance = tracker.generate_user_guidance()
# Should include macOS-specific commands (Homebrew)
assert "brew" in guidance.lower() or "macos" in guidance.lower()
def test_linux_setup_workflow(
self, mock_project_root, sample_python_files, mock_platform_system
):
"""
E2E: Setup workflow on Linux.
Expected: Linux-specific installation guidance.
"""
from lsp_setup.status_tracker import StatusTracker
mock_platform_system.return_value = "Linux"
with patch("platform.system", mock_platform_system):
with patch("shutil.which", return_value=None):
tracker = StatusTracker(mock_project_root, ["python"])
guidance = tracker.generate_user_guidance()
# Should include Linux-specific commands (apt/dnf)
assert (
"apt" in guidance.lower()
or "dnf" in guidance.lower()
or "linux" in guidance.lower()
)
class TestUserExperience:
"""E2E tests for user experience scenarios."""
def test_clear_progress_reporting(
self,
mock_project_root,
sample_python_files,
installed_lsp_binaries,
mock_npx_cclsp_success,
):
"""
E2E: User sees clear progress through setup.
Expected: Percentage completion updates correctly.
"""
from lsp_setup.language_detector import LanguageDetector
from lsp_setup.lsp_configurator import LSPConfigurator
from lsp_setup.plugin_manager import PluginManager
from lsp_setup.status_tracker import StatusTracker
detector = LanguageDetector(mock_project_root)
languages = detector.detect_languages()
with patch("shutil.which", side_effect=lambda x: installed_lsp_binaries.get(x)):
with patch("subprocess.run", mock_npx_cclsp_success):
tracker = StatusTracker(mock_project_root, languages)
# Initial progress
progress_0 = tracker.get_completion_percentage()
# After plugin install
manager = PluginManager()
manager.install_plugins(languages)
progress_1 = tracker.get_completion_percentage()
# After .env config
configurator = LSPConfigurator(mock_project_root)
configurator.enable_lsp()
progress_2 = tracker.get_completion_percentage()
# Progress should increase
assert progress_1 > progress_0
assert progress_2 > progress_1
assert progress_2 == 100
def test_helpful_error_messages(self, mock_project_root, sample_python_files):
"""
E2E: User encounters errors, gets helpful error messages.
Expected: Clear, actionable error messages.
"""
from lsp_setup.plugin_manager import PluginManager
with patch("shutil.which", return_value=None):
manager = PluginManager()
# npx not available
is_available = manager.is_npx_available()
assert is_available is False
# Should provide helpful guidance
# (In real implementation, this would be in error messages)