Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion agentstack/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from agentstack import conf, log
from agentstack.exceptions import EnvironmentError
from agentstack.utils import is_snake_case
from agentstack.utils import clean_input, is_snake_case
from agentstack import packaging
from agentstack import frameworks
from agentstack import generation
Expand Down Expand Up @@ -122,6 +122,9 @@ def init_project(
if not slug_name:
slug_name = prompt_slug_name()

# Keep the filesystem path aligned with the slug used by Cookiecutter.
slug_name = clean_input(slug_name)

conf.set_path(conf.PATH / slug_name)
# cookiecutter requires the directory to not exist
assert not os.path.exists(conf.PATH), f"Directory already exists: {conf.PATH}"
Expand Down
5 changes: 3 additions & 2 deletions agentstack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def open_yaml_file(path) -> dict:

def clean_input(input_string):
special_char_pattern = re.compile(r'[^a-zA-Z0-9\s_]')
return re.sub(special_char_pattern, '', input_string).lower().replace(' ', '_').replace('-', '_')
normalized_input = input_string.replace('-', '_')
return re.sub(special_char_pattern, '', normalized_input).lower().replace(' ', '_')


def term_color(text: str, color: str) -> str:
Expand Down Expand Up @@ -131,4 +132,4 @@ def get_base_dir():
except (RuntimeError, OSError, PermissionError):
# In CI or when directory is not writable, use temp directory
base_dir = Path(os.getenv('TEMP', '/tmp'))
return base_dir
return base_dir
25 changes: 25 additions & 0 deletions tests/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from parameterized import parameterized
from pathlib import Path
import shutil
import tempfile
from unittest.mock import patch
from cli_test_utils import run_cli
from agentstack import conf
from agentstack import frameworks
Expand Down Expand Up @@ -32,6 +34,29 @@ def test_init_command(self, template_name: str):
self.assertEqual(result.returncode, 0)
self.assertTrue((self.project_dir / 'test_project').exists())

def test_init_command_normalizes_hyphenated_project_name(self):
"""The CLI path must match the normalized Cookiecutter project slug."""
with tempfile.TemporaryDirectory() as temp_dir:
original_path = conf.PATH
conf.set_path(temp_dir)
try:
with patch('agentstack.cli.init.require_uv'), \
patch('agentstack.cli.init.welcome_message'), \
patch('agentstack.cli.init.insert_template') as insert_template, \
patch('agentstack.cli.init.packaging.create_venv'), \
patch('agentstack.cli.init.packaging.install_project'), \
patch('agentstack.cli.init.repo.init'), \
patch('agentstack.cli.init.repo.find_parent_repo', return_value=None), \
patch('agentstack.cli.init.repo.Transaction') as transaction:
transaction.return_value.__enter__.return_value = transaction.return_value
init_project(slug_name='test-project', template='empty', framework='crewai')

self.assertEqual(conf.PATH, Path(temp_dir) / 'test_project')
insert_template.assert_called_once()
self.assertEqual(insert_template.call_args.kwargs['name'], 'test_project')
finally:
conf.set_path(original_path)

@parameterized.expand([(k, v) for k, v in frameworks.ALIASED_FRAMEWORKS.items()])
def test_init_command_aliased_framework_empty_project(self, alias: str, framework: str):
"""Test the 'init' command with an aliased framework."""
Expand Down
6 changes: 5 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def test_clean_input_remove_space(self):
cleaned = clean_input('test project')
self.assertEqual('test_project', cleaned)

def test_clean_input_normalize_hyphen(self):
cleaned = clean_input('test-project')
self.assertEqual('test_project', cleaned)

def test_is_snake_case(self):
assert is_snake_case("hello_world")
assert not is_snake_case("HelloWorld")
Expand Down Expand Up @@ -69,4 +73,4 @@ def test_get_base_dir_writable(self, mock_user_data_dir):
result = get_base_dir()

self.assertIsInstance(result, Path)
self.assertTrue(result.is_absolute())
self.assertTrue(result.is_absolute())