Skip to content

Commit 0c622b9

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 caa5731 Snapshot before auto-ghstack a532f7b Snapshot before auto-format f7d4d34 Auto-commit format changes HEAD Auto-commit lint changes ``` codemcp-id: 281-feat-allow-running-from-non-root-directories ghstack-source-id: 443ba47 Pull-Request-resolved: #275
1 parent 3837cf4 commit 0c622b9

File tree

3 files changed

+111
-8
lines changed

3 files changed

+111
-8
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: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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,18 @@ 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(
870+
f"Error: No codemcp.toml file found in {path} or any parent directory",
871+
err=True,
872+
)
870873
return
871874

875+
# Use the project root for config and operations
876+
config_path = os.path.join(project_root, "codemcp.toml")
877+
872878
# Load command from config
873879
try:
874880
with open(config_path, "rb") as f:
@@ -898,10 +904,10 @@ def run(command: str, args: tuple, path: str) -> None:
898904
if args:
899905
cmd_list = list(cmd_list) + list(args)
900906

901-
# Run the command with inherited stdin/stdout/stderr
907+
# Run the command with inherited stdin/stdout/stderr from the project root
902908
process = subprocess.run(
903909
cmd_list,
904-
cwd=full_path,
910+
cwd=project_root, # Use project root as working directory
905911
stdin=None, # inherit
906912
stdout=None, # inherit
907913
stderr=None, # inherit

e2e/test_run_from_subdir.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 show the project root, not the subdirectory
49+
# Normalize paths for comparison (strip trailing slashes, etc.)
50+
normalized_output = os.path.normpath(result.stdout.strip())
51+
normalized_project_dir = os.path.normpath(project_dir)
52+
assert normalized_output == normalized_project_dir
53+
54+
55+
def test_run_command_from_project_root(project_dir):
56+
"""Test running a command from the project root."""
57+
result = subprocess.run(
58+
[sys.executable, "-m", "codemcp", "run", "echo", "--path", project_dir],
59+
capture_output=True,
60+
text=True,
61+
check=True,
62+
)
63+
64+
assert "Hello World" in result.stdout

0 commit comments

Comments
 (0)