diff --git a/concore_cli/commands/init.py b/concore_cli/commands/init.py index 53fd53f..cfaf73c 100644 --- a/concore_cli/commands/init.py +++ b/concore_cli/commands/init.py @@ -258,8 +258,14 @@ def _build_graphml(project_name, selected_langs): # --------------------------------------------------------------------------- +def _check_project_name(name): + if name in (".", "..") or Path(name).name != name: + raise ValueError("Project name must not contain path separators") + + def init_project_interactive(name, selected_langs, console): """Create a project with one node per selected language (no edges).""" + _check_project_name(name) project_path = Path(name) if project_path.exists(): @@ -327,6 +333,7 @@ def init_project_interactive(name, selected_langs, console): def init_project(name, template, console): """Non-interactive init — single Python node skeleton.""" + _check_project_name(name) project_path = Path(name) if project_path.exists(): diff --git a/tests/test_cli.py b/tests/test_cli.py index d746040..e6d7ab2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,12 +2,15 @@ import tempfile import shutil import os +import sys import json from pathlib import Path from unittest.mock import patch from click.testing import CliRunner from concore_cli.cli import cli +cli_module = sys.modules[cli.callback.__module__] + class TestConcoreCLI(unittest.TestCase): def setUp(self): @@ -55,6 +58,19 @@ def test_init_existing_directory(self): self.assertNotEqual(result.exit_code, 0) self.assertIn("already exists", result.output) + def test_init_rejects_path_like_name(self): + # init must not create the project outside the current directory (#588) + for args in (["init", "../x"], ["init", "../x", "--interactive"]): + with self.subTest(args=args): + with self.runner.isolated_filesystem(temp_dir=self.temp_dir): + with patch.object( + cli_module, "run_wizard", return_value=["python"] + ): + result = self.runner.invoke(cli, args) + self.assertNotEqual(result.exit_code, 0) + self.assertIn("path separators", result.output) + self.assertFalse(Path("../x").exists()) + def test_validate_missing_file(self): result = self.runner.invoke(cli, ["validate", "nonexistent.graphml"]) self.assertNotEqual(result.exit_code, 0)