diff --git a/codemcp/__init__.py b/codemcp/__init__.py index 956dde18..a9ee2048 100644 --- a/codemcp/__init__.py +++ b/codemcp/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from .hot_reload_entry import run as run_hot_reload -from .main import codemcp, configure_logging, mcp, run +from .main import cli, codemcp, configure_logging, mcp, run from .shell import get_subprocess_env, run_command __all__ = [ @@ -12,4 +12,5 @@ "codemcp", "run_command", "get_subprocess_env", + "cli", ] diff --git a/codemcp/__main__.py b/codemcp/__main__.py index 4f775249..346061fb 100644 --- a/codemcp/__main__.py +++ b/codemcp/__main__.py @@ -2,9 +2,10 @@ # WARNING: do NOT do a relative import, this file must be directly executable # by filename -from codemcp import mcp, run +from codemcp import cli, mcp __all__ = ["mcp"] if __name__ == "__main__": - run() + # Use Click's CLI interface instead of directly calling run() + cli() diff --git a/codemcp/main.py b/codemcp/main.py index 5dc15cc7..54677917 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -2,7 +2,11 @@ import logging import os +import sys +from pathlib import Path +from typing import Optional +import click from mcp.server.fastmcp import FastMCP from .tools.chmod import chmod @@ -406,6 +410,91 @@ def filter(self, record): logging.info("Logs from 'mcp' module are being filtered") +def init_codemcp_project(path: str) -> str: + """Initialize a new codemcp project. + + Args: + path: Path to initialize the project in + + Returns: + Message indicating success or failure + """ + import subprocess + + try: + # Convert to Path object and resolve to absolute path + project_path = Path(path).resolve() + + # Create directory if it doesn't exist + project_path.mkdir(parents=True, exist_ok=True) + + # Check if git repository already exists + git_dir = project_path / ".git" + if not git_dir.exists(): + # Initialize git repository + subprocess.run(["git", "init"], cwd=project_path, check=True) + print(f"Initialized git repository in {project_path}") + else: + print(f"Git repository already exists in {project_path}") + + # Create empty codemcp.toml file if it doesn't exist + config_file = project_path / "codemcp.toml" + if not config_file.exists(): + with open(config_file, "w") as f: + f.write("# codemcp configuration file\n\n") + print(f"Created empty codemcp.toml file in {project_path}") + else: + print(f"codemcp.toml file already exists in {project_path}") + + # Make initial commit if there are no commits yet + try: + # Check if there are any commits + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=project_path, + check=False, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + # No commits yet, add codemcp.toml and make initial commit + subprocess.run( + ["git", "add", "codemcp.toml"], cwd=project_path, check=True + ) + subprocess.run( + ["git", "commit", "-m", "chore: initialize codemcp project"], + cwd=project_path, + check=True, + ) + print("Created initial commit with codemcp.toml") + else: + print("Repository already has commits, not creating initial commit") + except subprocess.CalledProcessError as e: + print(f"Warning: Failed to create initial commit: {e}") + + return f"Successfully initialized codemcp project in {project_path}" + except Exception as e: + return f"Error initializing project: {e}" + + +@click.group(invoke_without_command=True) +@click.pass_context +def cli(ctx): + """CodeMCP: Command-line interface for MCP server and project management.""" + # If no subcommand is provided, run the MCP server (for backwards compatibility) + if ctx.invoked_subcommand is None: + run() + + +@cli.command() +@click.argument("path", type=click.Path(), default=".") +def init(path): + """Initialize a new codemcp project with an empty codemcp.toml file and git repository.""" + result = init_codemcp_project(path) + click.echo(result) + + def run(): """Run the MCP server.""" configure_logging() diff --git a/e2e/test_init_command.py b/e2e/test_init_command.py new file mode 100644 index 00000000..80b70371 --- /dev/null +++ b/e2e/test_init_command.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +import subprocess +import tempfile +from pathlib import Path + +from codemcp.main import init_codemcp_project + + +def test_init_command(): + """Test the init command creates a codemcp.toml file and initializes a git repo.""" + # Create a temporary directory for testing + with tempfile.TemporaryDirectory() as temp_dir: + # Run the init_codemcp_project function + result = init_codemcp_project(temp_dir) + + # Check that the function reports success + assert "Successfully initialized" in result + + # Check that codemcp.toml was created + config_file = Path(temp_dir) / "codemcp.toml" + assert config_file.exists() + + # Check that git repository was initialized + git_dir = Path(temp_dir) / ".git" + assert git_dir.is_dir() + + # Check that a commit was created + result = subprocess.run( + ["git", "log", "--oneline"], + cwd=temp_dir, + capture_output=True, + text=True, + check=True, + ) + assert "initialize codemcp project" in result.stdout + + +def test_init_command_existing_repo(): + """Test the init command works with an existing git repository.""" + # Create a temporary directory for testing + with tempfile.TemporaryDirectory() as temp_dir: + # Initialize git repository first + subprocess.run(["git", "init"], cwd=temp_dir, check=True) + + # Make a dummy commit to simulate existing repository + dummy_file = Path(temp_dir) / "dummy.txt" + dummy_file.write_text("test content") + + subprocess.run( + ["git", "config", "user.name", "Test User"], cwd=temp_dir, check=True + ) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], + cwd=temp_dir, + check=True, + ) + subprocess.run(["git", "add", "dummy.txt"], cwd=temp_dir, check=True) + subprocess.run( + ["git", "commit", "-m", "Initial commit"], cwd=temp_dir, check=True + ) + + # Run the init_codemcp_project function + result = init_codemcp_project(temp_dir) + + # Check that the function reports success + assert "Successfully initialized" in result + + # Check that codemcp.toml was created + config_file = Path(temp_dir) / "codemcp.toml" + assert config_file.exists() diff --git a/pyproject.toml b/pyproject.toml index 538419d8..7c90a130 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pyyaml>=6.0.0", "pytest-xdist>=3.6.1", "editorconfig>=0.17.0", + "click>=8.1.8", ] [dependency-groups] @@ -28,7 +29,7 @@ dev = [ ] [project.scripts] -codemcp = "codemcp:run" +codemcp = "codemcp:cli" codemcp-multi = "codemcp.multi_entry:main" [build-system] diff --git a/uv.lock b/uv.lock index 7bdcc81b..4affd1d9 100644 --- a/uv.lock +++ b/uv.lock @@ -76,6 +76,7 @@ version = "0.5.0" source = { editable = "." } dependencies = [ { name = "anyio" }, + { name = "click" }, { name = "editorconfig" }, { name = "mcp", extra = ["cli"] }, { name = "pytest-xdist" }, @@ -99,6 +100,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=3.7.0" }, + { name = "click", specifier = ">=8.1.8" }, { name = "editorconfig", specifier = ">=0.17.0" }, { name = "mcp", extras = ["cli"], specifier = ">=1.2.0" }, { name = "pytest-xdist", specifier = ">=3.6.1" },