diff --git a/src/apm_cli/cli.py b/src/apm_cli/cli.py index 97c70c4b..1f93c2e0 100644 --- a/src/apm_cli/cli.py +++ b/src/apm_cli/cli.py @@ -336,6 +336,7 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False): current_deps = data['dependencies']['apm'] or [] validated_packages = [] + conflicts = [] # First, validate all packages _rich_info(f"Validating {len(packages)} package(s)...") @@ -346,9 +347,10 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False): _rich_error(f"Invalid package format: {package}. Use 'owner/repo' format.") continue - # Check if package is already in dependencies + # Check if package is already in dependencies -> treat as conflict if package in current_deps: - _rich_warning(f"Package {package} already exists in apm.yml") + conflicts.append(package) + _rich_error(f"Package conflict: {package} already exists in apm.yml") continue # Validate package exists and is accessible @@ -359,6 +361,10 @@ def _validate_and_add_packages_to_apm_yml(packages, dry_run=False): _rich_error(f"✗ {package} - not accessible or doesn't exist") if not validated_packages: + if conflicts and not dry_run: + _rich_error(f"Aborting due to {len(conflicts)} package conflict(s)") + sys.exit(1) + if dry_run: _rich_warning("No new valid packages to add") return [] @@ -445,10 +451,25 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run): apm install --dry-run # Show what would be installed """ try: - # Check if apm.yml exists - if not Path('apm.yml').exists(): - _rich_error("No apm.yml found. Run 'apm init' first.") - sys.exit(1) + # Check if apm.yml exists. If packages were provided, create a minimal + # project (apm.yml + .apm/ structure) so the install command can add + # package entries automatically. Otherwise, require the user to run + # 'apm init' first. + apm_yml_path = Path('apm.yml') + if not apm_yml_path.exists(): + if packages: + _rich_info("No apm.yml found - creating minimal project so packages can be added") + # Create a minimal default config and project files + try: + default_config = _get_default_config(Path.cwd().name) + _create_project_files(default_config) + _rich_success("Created minimal apm.yml and .apm/ structure") + except Exception as e: + _rich_error(f"Failed to create minimal project files: {e}") + sys.exit(1) + else: + _rich_error("No apm.yml found. Run 'apm init' first.") + sys.exit(1) # If packages are specified, validate and add them to apm.yml first if packages: diff --git a/tests/test_install_auto_init.py b/tests/test_install_auto_init.py new file mode 100644 index 00000000..d8680c03 --- /dev/null +++ b/tests/test_install_auto_init.py @@ -0,0 +1,65 @@ +import os +from pathlib import Path + +import yaml +import pytest +from click.testing import CliRunner + +import apm_cli.cli as cli_mod +from apm_cli.cli import cli + + +def test_install_creates_minimal_project(tmp_path, monkeypatch): + """When apm.yml is missing and a package is passed to `apm install`, + the CLI should create a minimal apm.yml and .apm/ structure so the + installation can proceed (dry-run used to avoid network operations). + """ + runner = CliRunner() + # Avoid network/git checks during validation + monkeypatch.setattr(cli_mod, "_validate_package_exists", lambda pkg: True) + + # CliRunner.invoke in this environment does not accept cwd reliably, so + # change directory manually for the duration of the invocation. + old_cwd = Path.cwd() + try: + os.chdir(tmp_path) + result = runner.invoke(cli, ["install", "owner/repo", "--dry-run"]) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0, result.output + + apm_yml = tmp_path / "apm.yml" + assert apm_yml.exists(), "apm.yml should be created when running install with packages" + + # Load config to ensure it is valid YAML and contains expected keys + cfg = yaml.safe_load(apm_yml.read_text()) + assert isinstance(cfg, dict) + assert "dependencies" in cfg + + +def test_install_conflict_aborts(tmp_path, monkeypatch): + """If the package already exists in apm.yml, `apm install ` should + report a conflict and abort (non-zero exit).""" + runner = CliRunner() + # Create an apm.yml that already contains the package + content = { + "name": "proj", + "version": "1.0.0", + "dependencies": {"apm": ["owner/repo"], "mcp": []}, + } + apm_yml = tmp_path / "apm.yml" + apm_yml.write_text(yaml.safe_dump(content)) + + # Avoid network/git checks during validation + monkeypatch.setattr(cli_mod, "_validate_package_exists", lambda pkg: True) + + old_cwd = Path.cwd() + try: + os.chdir(tmp_path) + result = runner.invoke(cli, ["install", "owner/repo"]) + finally: + os.chdir(old_cwd) + + # The CLI should exit with non-zero status due to conflict + assert result.exit_code != 0 + assert "Package conflict" in result.output or "Aborting due to" in result.output