From 9ce7c84e9209f1ee43a1980097e155a112bb0378 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:32:55 +0000 Subject: [PATCH 1/5] Initial plan From 64ad3f3105aea60bfcc40b9ef894891e18bfe251 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:40:35 +0000 Subject: [PATCH 2/5] Implement auto-bootstrap in apm install command Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- src/apm_cli/cli.py | 17 +- tests/unit/test_install_command.py | 239 +++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_install_command.py diff --git a/src/apm_cli/cli.py b/src/apm_cli/cli.py index ce9b2ea5..c8c3f4e8 100644 --- a/src/apm_cli/cli.py +++ b/src/apm_cli/cli.py @@ -455,8 +455,21 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run): """ try: # Check if apm.yml exists - if not Path("apm.yml").exists(): - _rich_error("No apm.yml found. Run 'apm init' first.") + apm_yml_exists = Path("apm.yml").exists() + + # Auto-bootstrap: create minimal apm.yml when packages specified but no apm.yml + if not apm_yml_exists and packages: + # Get current directory name as project name + project_name = Path.cwd().name + config = _get_default_config(project_name) + _create_minimal_apm_yml(config) + _rich_success("Created apm.yml", symbol="sparkles") + + # Error when NO apm.yml AND NO packages + if not apm_yml_exists and not packages: + _rich_error("No apm.yml found") + _rich_info("💡 Run 'apm init' to create one, or:") + _rich_info(" apm install to auto-create + install") sys.exit(1) # If packages are specified, validate and add them to apm.yml first diff --git a/tests/unit/test_install_command.py b/tests/unit/test_install_command.py new file mode 100644 index 00000000..d373f8db --- /dev/null +++ b/tests/unit/test_install_command.py @@ -0,0 +1,239 @@ +"""Tests for the apm install command auto-bootstrap feature.""" + +import pytest +import tempfile +import os +import yaml +from pathlib import Path +from click.testing import CliRunner +from unittest.mock import patch, MagicMock + +from apm_cli.cli import cli + + +class TestInstallCommandAutoBootstrap: + """Test cases for apm install command auto-bootstrap feature.""" + + def setup_method(self): + """Set up test fixtures.""" + self.runner = CliRunner() + try: + self.original_dir = os.getcwd() + except FileNotFoundError: + self.original_dir = str(Path(__file__).parent.parent.parent) + os.chdir(self.original_dir) + + def teardown_method(self): + """Clean up after tests.""" + try: + os.chdir(self.original_dir) + except (FileNotFoundError, OSError): + repo_root = Path(__file__).parent.parent.parent + os.chdir(str(repo_root)) + + def test_install_no_apm_yml_no_packages_shows_helpful_error(self): + """Test that install without apm.yml and without packages shows helpful error.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + result = self.runner.invoke(cli, ["install"]) + + assert result.exit_code == 1 + assert "No apm.yml found" in result.output + assert "apm init" in result.output + assert "apm install " in result.output + + @patch("apm_cli.cli._validate_package_exists") + @patch("apm_cli.cli.APM_DEPS_AVAILABLE", True) + @patch("apm_cli.cli.APMPackage") + @patch("apm_cli.cli._install_apm_dependencies") + def test_install_no_apm_yml_with_packages_creates_minimal_apm_yml( + self, mock_install_apm, mock_apm_package, mock_validate, monkeypatch + ): + """Test that install with packages but no apm.yml creates minimal apm.yml.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + # Mock package validation to return True + mock_validate.return_value = True + + # Mock APMPackage to return empty dependencies + mock_pkg_instance = MagicMock() + mock_pkg_instance.get_apm_dependencies.return_value = [ + MagicMock(repo_url="test/package", reference="main") + ] + mock_pkg_instance.get_mcp_dependencies.return_value = [] + mock_apm_package.from_apm_yml.return_value = mock_pkg_instance + + # Mock the install function to avoid actual installation + mock_install_apm.return_value = None + + result = self.runner.invoke(cli, ["install", "test/package"]) + + # Should succeed and create apm.yml + assert result.exit_code == 0 + assert "Created apm.yml" in result.output + assert Path("apm.yml").exists() + + # Verify apm.yml structure + with open("apm.yml") as f: + config = yaml.safe_load(f) + assert "dependencies" in config + assert "apm" in config["dependencies"] + assert "test/package" in config["dependencies"]["apm"] + assert config["dependencies"]["mcp"] == [] + + @patch("apm_cli.cli._validate_package_exists") + @patch("apm_cli.cli.APM_DEPS_AVAILABLE", True) + @patch("apm_cli.cli.APMPackage") + @patch("apm_cli.cli._install_apm_dependencies") + def test_install_no_apm_yml_with_multiple_packages( + self, mock_install_apm, mock_apm_package, mock_validate, monkeypatch + ): + """Test that install with multiple packages creates apm.yml and adds all.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + # Mock package validation + mock_validate.return_value = True + + # Mock APMPackage + mock_pkg_instance = MagicMock() + mock_pkg_instance.get_apm_dependencies.return_value = [ + MagicMock(repo_url="org1/pkg1", reference="main"), + MagicMock(repo_url="org2/pkg2", reference="main"), + ] + mock_pkg_instance.get_mcp_dependencies.return_value = [] + mock_apm_package.from_apm_yml.return_value = mock_pkg_instance + + mock_install_apm.return_value = None + + result = self.runner.invoke(cli, ["install", "org1/pkg1", "org2/pkg2"]) + + assert result.exit_code == 0 + assert "Created apm.yml" in result.output + assert Path("apm.yml").exists() + + # Verify both packages are in apm.yml + with open("apm.yml") as f: + config = yaml.safe_load(f) + assert "org1/pkg1" in config["dependencies"]["apm"] + assert "org2/pkg2" in config["dependencies"]["apm"] + + @patch("apm_cli.cli.APM_DEPS_AVAILABLE", True) + @patch("apm_cli.cli.APMPackage") + @patch("apm_cli.cli._install_apm_dependencies") + def test_install_existing_apm_yml_preserves_behavior( + self, mock_install_apm, mock_apm_package + ): + """Test that install with existing apm.yml works as before.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + # Create existing apm.yml + existing_config = { + "name": "test-project", + "version": "1.0.0", + "description": "Test project", + "author": "Test Author", + "dependencies": {"apm": [], "mcp": []}, + "scripts": {}, + } + with open("apm.yml", "w") as f: + yaml.dump(existing_config, f) + + # Mock APMPackage + mock_pkg_instance = MagicMock() + mock_pkg_instance.get_apm_dependencies.return_value = [] + mock_pkg_instance.get_mcp_dependencies.return_value = [] + mock_apm_package.from_apm_yml.return_value = mock_pkg_instance + + result = self.runner.invoke(cli, ["install"]) + + # Should succeed and NOT show "Created apm.yml" + assert result.exit_code == 0 + assert "Created apm.yml" not in result.output + + # Verify original config is preserved + with open("apm.yml") as f: + config = yaml.safe_load(f) + assert config["name"] == "test-project" + assert config["author"] == "Test Author" + + @patch("apm_cli.cli._validate_package_exists") + @patch("apm_cli.cli.APM_DEPS_AVAILABLE", True) + @patch("apm_cli.cli.APMPackage") + @patch("apm_cli.cli._install_apm_dependencies") + def test_install_auto_created_apm_yml_has_correct_metadata( + self, mock_install_apm, mock_apm_package, mock_validate + ): + """Test that auto-created apm.yml has correct metadata.""" + with tempfile.TemporaryDirectory() as tmp_dir: + # Create a directory with a specific name to test project name detection + project_dir = Path(tmp_dir) / "my-awesome-project" + project_dir.mkdir() + os.chdir(project_dir) + + # Mock validation and installation + mock_validate.return_value = True + + mock_pkg_instance = MagicMock() + mock_pkg_instance.get_apm_dependencies.return_value = [ + MagicMock(repo_url="test/package", reference="main") + ] + mock_pkg_instance.get_mcp_dependencies.return_value = [] + mock_apm_package.from_apm_yml.return_value = mock_pkg_instance + + mock_install_apm.return_value = None + + result = self.runner.invoke(cli, ["install", "test/package"]) + + assert result.exit_code == 0 + assert Path("apm.yml").exists() + + # Verify auto-detected project name + with open("apm.yml") as f: + config = yaml.safe_load(f) + assert config["name"] == "my-awesome-project" + assert "version" in config + assert "description" in config + assert "APM project" in config["description"] + + @patch("apm_cli.cli._validate_package_exists") + def test_install_invalid_package_format_with_no_apm_yml(self, mock_validate): + """Test that invalid package format fails gracefully even with auto-bootstrap.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + # Don't mock validation - let it handle invalid format + result = self.runner.invoke(cli, ["install", "invalid-package"]) + + # Should create apm.yml but fail to add invalid package + assert Path("apm.yml").exists() + assert "Invalid package format" in result.output + + @patch("apm_cli.cli._validate_package_exists") + @patch("apm_cli.cli.APM_DEPS_AVAILABLE", True) + @patch("apm_cli.cli.APMPackage") + @patch("apm_cli.cli._install_apm_dependencies") + def test_install_dry_run_with_no_apm_yml_shows_what_would_be_created( + self, mock_install_apm, mock_apm_package, mock_validate + ): + """Test that dry-run with no apm.yml shows what would be created.""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + + mock_validate.return_value = True + + mock_pkg_instance = MagicMock() + mock_pkg_instance.get_apm_dependencies.return_value = [] + mock_pkg_instance.get_mcp_dependencies.return_value = [] + mock_apm_package.from_apm_yml.return_value = mock_pkg_instance + + result = self.runner.invoke(cli, ["install", "test/package", "--dry-run"]) + + # Should show what would be added + assert result.exit_code == 0 + assert "Would add" in result.output or "Dry run" in result.output + # apm.yml should still be created (for dry-run to work) + assert Path("apm.yml").exists() From de5101f6473fcf8f60328c08d788211e6893104f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:44:05 +0000 Subject: [PATCH 3/5] Update CLI reference documentation for auto-bootstrap feature Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- docs/cli-reference.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 6d019997..72143f76 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -106,14 +106,17 @@ apm init my-project --yes ### `apm install` - 📦 Install APM and MCP dependencies -Install APM package and MCP server dependencies from `apm.yml` (like `npm install`). +Install APM package and MCP server dependencies from `apm.yml` (like `npm install`). Auto-creates minimal `apm.yml` when packages are specified but no manifest exists. ```bash -apm install [OPTIONS] +apm install [PACKAGES...] [OPTIONS] ``` +**Arguments:** +- `PACKAGES` - Optional APM packages to add and install (format: `owner/repo`) + **Options:** -- `--runtime TEXT` - Target specific runtime only (codex, vscode) +- `--runtime TEXT` - Target specific runtime only (copilot, codex, vscode) - `--exclude TEXT` - Exclude specific runtime from installation - `--only [apm|mcp]` - Install only specific dependency type - `--update` - Update dependencies to latest Git references @@ -124,6 +127,12 @@ apm install [OPTIONS] # Install all dependencies from apm.yml apm install +# Auto-create apm.yml and install package (no init needed!) +apm install danielmeppiel/design-guidelines + +# Add multiple packages and install +apm install org/pkg1 org/pkg2 + # Install only APM dependencies (skip MCP servers) apm install --only=apm @@ -140,12 +149,15 @@ apm install --update apm install --exclude codex ``` +**Auto-Bootstrap Behavior:** +- **With packages + no apm.yml**: Automatically creates minimal `apm.yml`, adds packages, and installs +- **Without packages + no apm.yml**: Shows helpful error suggesting `apm init` or `apm install ` +- **With apm.yml**: Works as before - installs existing dependencies or adds new packages + **Dependency Types:** - **APM Dependencies**: GitHub repositories containing `.apm/` context collections - **MCP Dependencies**: Model Context Protocol servers for runtime integration -**Requirements:** Must be run in a directory with `apm.yml` file. - **Working Example with Dependencies:** ```yaml # Example apm.yml with APM dependencies From b03690a6a29d1989c68e6442751e5a6ae00b6c57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:46:15 +0000 Subject: [PATCH 4/5] Update install command help text to mention auto-bootstrap Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- src/apm_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apm_cli/cli.py b/src/apm_cli/cli.py index c8c3f4e8..2f3dca2d 100644 --- a/src/apm_cli/cli.py +++ b/src/apm_cli/cli.py @@ -420,7 +420,7 @@ def _validate_package_exists(package): return False -@cli.command(help="Install APM and MCP dependencies from apm.yml") +@cli.command(help="📦 Install APM and MCP dependencies (auto-creates apm.yml if needed)") @click.argument("packages", nargs=-1) @click.option("--runtime", help="Target specific runtime only (copilot, codex, vscode)") @click.option("--exclude", help="Exclude specific runtime from installation") From b304f1b821f263f19cb7ac7913c4a212a010c100 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:48:21 +0000 Subject: [PATCH 5/5] Clarify help text based on code review feedback Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- src/apm_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apm_cli/cli.py b/src/apm_cli/cli.py index 2f3dca2d..649a336e 100644 --- a/src/apm_cli/cli.py +++ b/src/apm_cli/cli.py @@ -420,7 +420,7 @@ def _validate_package_exists(package): return False -@cli.command(help="📦 Install APM and MCP dependencies (auto-creates apm.yml if needed)") +@cli.command(help="📦 Install APM and MCP dependencies (auto-creates apm.yml when installing packages)") @click.argument("packages", nargs=-1) @click.option("--runtime", help="Target specific runtime only (copilot, codex, vscode)") @click.option("--exclude", help="Exclude specific runtime from installation")