Skip to content

Commit caa5731

Browse files
committed
feat: allow running from non-root directories
Make 'codemcp run' work even when not in the root dir of the project, by traversing up to project root to find codemcp.toml. Use preexisting functionality to do this. ```git-revs fb6f20a (Base revision) cfd70e1 Snapshot before codemcp change 477cfe9 Snapshot before codemcp change e9001c6 Snapshot before auto-ghstack 0b8709d Snapshot before codemcp change HEAD Snapshot before auto-ghstack ``` codemcp-id: 281-feat-allow-running-from-non-root-directories ghstack-source-id: b6ae8dc Pull-Request-resolved: #275
1 parent e276cb2 commit caa5731

File tree

3 files changed

+107
-9
lines changed

3 files changed

+107
-9
lines changed

codemcp/common.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22

33
import os
4-
from typing import List, Union
4+
from typing import List, Optional, Union
55

66
# Constants
77
MAX_LINES_TO_READ = 1000
@@ -19,6 +19,7 @@
1919
"normalize_file_path",
2020
"get_edit_snippet",
2121
"truncate_output_content",
22+
"find_codemcp_root",
2223
]
2324

2425

@@ -157,3 +158,35 @@ def truncate_output_content(
157158
truncated_content += f"\n... (output truncated, showing {MAX_LINES_TO_READ} of {total_lines} lines)"
158159

159160
return truncated_content
161+
162+
163+
def find_codemcp_root(start_path: str) -> Optional[str]:
164+
"""Find the root directory containing codemcp.toml, starting from the given path.
165+
166+
This function traverses up the directory tree from the given path until it finds
167+
a directory containing a codemcp.toml file, which indicates the project root.
168+
169+
Args:
170+
start_path: The path to start searching from (can be a file or directory)
171+
172+
Returns:
173+
The absolute path to the directory containing codemcp.toml, or None if not found
174+
"""
175+
path = os.path.abspath(start_path)
176+
177+
# If the path is a file, start from its parent directory
178+
if os.path.isfile(path):
179+
path = os.path.dirname(path)
180+
181+
while path:
182+
config_path = os.path.join(path, "codemcp.toml")
183+
if os.path.isfile(config_path):
184+
return path
185+
186+
parent = os.path.dirname(path)
187+
if parent == path: # Reached filesystem root
188+
return None
189+
190+
path = parent
191+
192+
return None

codemcp/main.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from starlette.applications import Starlette
1515
from starlette.routing import Mount
1616

17-
from .common import normalize_file_path
17+
from .common import find_codemcp_root, normalize_file_path
1818
from .git_query import get_current_commit_hash
1919
from .tools.chmod import chmod
2020
from .tools.edit_file import edit_file_content
@@ -845,7 +845,7 @@ def run(command: str, args: tuple, path: str) -> None:
845845

846846
import tomli
847847

848-
from .common import normalize_file_path
848+
from .common import find_codemcp_root, normalize_file_path
849849

850850
# Handle the async nature of the function in a sync context
851851
asyncio.get_event_loop()
@@ -863,12 +863,15 @@ def run(command: str, args: tuple, path: str) -> None:
863863
click.echo(f"Error: Path {path} is not a directory", err=True)
864864
return
865865

866-
# Check for codemcp.toml file
867-
config_path = os.path.join(full_path, "codemcp.toml")
868-
if not os.path.exists(config_path):
869-
click.echo(f"Error: Config file not found: {config_path}", err=True)
866+
# Find project root by traversing up to find codemcp.toml
867+
project_root = find_codemcp_root(full_path)
868+
if project_root is None:
869+
click.echo(f"Error: No codemcp.toml file found in {path} or any parent directory", err=True)
870870
return
871871

872+
# Use the project root for config and operations
873+
config_path = os.path.join(project_root, "codemcp.toml")
874+
872875
# Load command from config
873876
try:
874877
with open(config_path, "rb") as f:
@@ -898,10 +901,10 @@ def run(command: str, args: tuple, path: str) -> None:
898901
if args:
899902
cmd_list = list(cmd_list) + list(args)
900903

901-
# Run the command with inherited stdin/stdout/stderr
904+
# Run the command with inherited stdin/stdout/stderr from the project root
902905
process = subprocess.run(
903906
cmd_list,
904-
cwd=full_path,
907+
cwd=project_root, # Use project root as working directory
905908
stdin=None, # inherit
906909
stdout=None, # inherit
907910
stderr=None, # inherit

e2e/test_run_from_subdir.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import subprocess
5+
import sys
6+
import tempfile
7+
8+
import pytest
9+
10+
from codemcp.main import init_codemcp_project
11+
12+
13+
@pytest.fixture
14+
def project_dir():
15+
"""Create a temporary project directory with a simple codemcp.toml configuration."""
16+
with tempfile.TemporaryDirectory() as temp_dir:
17+
# Initialize the project
18+
init_codemcp_project(temp_dir)
19+
20+
# Create a codemcp.toml file with test commands
21+
config_path = os.path.join(temp_dir, "codemcp.toml")
22+
with open(config_path, "w") as f:
23+
f.write("""
24+
[commands]
25+
echo = ["echo", "Hello World"]
26+
pwd = ["pwd"]
27+
""")
28+
29+
# Create a subdirectory
30+
subdir = os.path.join(temp_dir, "subdir")
31+
os.makedirs(subdir, exist_ok=True)
32+
33+
yield temp_dir
34+
35+
36+
def test_run_command_from_subdir(project_dir):
37+
"""Test running a command from a subdirectory of the project."""
38+
subdir = os.path.join(project_dir, "subdir")
39+
40+
# Run pwd command from subdirectory to verify cwd
41+
result = subprocess.run(
42+
[sys.executable, "-m", "codemcp", "run", "pwd", "--path", subdir],
43+
capture_output=True,
44+
text=True,
45+
check=True,
46+
)
47+
48+
# The pwd output should be the project root, not the subdirectory
49+
assert project_dir in result.stdout
50+
assert "subdir" not in result.stdout
51+
52+
53+
def test_run_command_from_project_root(project_dir):
54+
"""Test running a command from the project root."""
55+
result = subprocess.run(
56+
[sys.executable, "-m", "codemcp", "run", "echo", "--path", project_dir],
57+
capture_output=True,
58+
text=True,
59+
check=True,
60+
)
61+
62+
assert "Hello World" in result.stdout

0 commit comments

Comments
 (0)