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
24 changes: 17 additions & 7 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ apm pack [OPTIONS]

**Options:**
- `-o, --output PATH` - Output directory (default: `./build`)
- `-t, --target [copilot|vscode|claude|all]` - Filter files by target. Auto-detects from `apm.yml` if not specified. `vscode` is an alias for `copilot`
- `-t, --target [vscode|copilot|claude|all]` - Filter files by target. Auto-detects from `apm.yml` if not specified. `copilot` is an alias for `vscode`
- `--archive` - Produce a `.tar.gz` archive instead of a directory
- `--dry-run` - List files that would be packed without writing anything
- `--format [apm|plugin]` - Bundle format (default: `apm`)
Expand Down Expand Up @@ -567,17 +567,27 @@ apm deps info design-guidelines
Remove the entire `apm_modules/` directory and all installed APM packages.

```bash
apm deps clean
apm deps clean [OPTIONS]
```

**Options:**
- `--dry-run` - Show what would be removed without removing
- `--yes`, `-y` - Skip confirmation prompt (for non-interactive/scripted use)

Comment on lines +570 to +576
**Examples:**
```bash
# Remove all APM dependencies (with confirmation)
apm deps clean

# Preview what would be removed
apm deps clean --dry-run

# Remove without confirmation (e.g. in CI pipelines)
apm deps clean --yes
```

**Behavior:**
- Shows confirmation prompt before deletion
- Shows confirmation prompt before deletion (unless `--yes` is provided)
- Removes entire `apm_modules/` directory
- Displays count of packages that will be removed
- Can be cancelled with Ctrl+C or 'n' response
Expand All @@ -587,11 +597,11 @@ apm deps clean
Update installed APM dependencies to their latest versions.

```bash
apm deps update [PACKAGE_NAME]
apm deps update [PACKAGE]
```

**Arguments:**
- `PACKAGE_NAME` - Optional. Update specific package only
- `PACKAGE` - Optional. Update specific package only

**Examples:**
```bash
Expand Down Expand Up @@ -619,7 +629,7 @@ apm mcp list [OPTIONS]
```

**Options:**
- `--limit INTEGER` - Number of results to show
- `--limit INTEGER` - Number of results to show (default: 20)

**Examples:**
```bash
Expand Down Expand Up @@ -1088,7 +1098,7 @@ apm runtime remove [OPTIONS] {copilot|codex|llm}
**Options:**
- `--yes` - Confirm the action without prompting

#### `apm runtime status` - Show runtime status
#### `apm runtime status` - Show active runtime and preference order

Display which runtime APM will use for execution and runtime preference order.

Expand Down
37 changes: 24 additions & 13 deletions src/apm_cli/commands/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@ def _add_children(parent_branch, parent_repo_url, depth=0):


@deps.command(help="Remove all APM dependencies")
def clean():
@click.option("--dry-run", is_flag=True, default=False, help="Show what would be removed without removing")
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompt")
def clean(dry_run: bool, yes: bool):
Comment on lines 392 to +395
"""Remove entire apm_modules/ directory."""
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
Expand All @@ -399,21 +401,30 @@ def clean():
_rich_info("No apm_modules/ directory found - already clean")
return

# Show what will be removed
package_count = len([d for d in apm_modules_path.iterdir() if d.is_dir()])
# Count actual installed packages (not just top-level dirs like org namespaces or _local)
from ._helpers import _scan_installed_packages
packages = _scan_installed_packages(apm_modules_path)
package_count = len(packages)

_rich_warning(f"This will remove the entire apm_modules/ directory ({package_count} packages)")
if dry_run:
_rich_info(f"Dry run: would remove apm_modules/ ({package_count} package(s))")
for pkg in sorted(packages):
_rich_info(f" - {pkg}")
return

# Confirmation prompt
try:
from rich.prompt import Confirm
confirm = Confirm.ask("Continue?")
except ImportError:
confirm = click.confirm("Continue?")
_rich_warning(f"This will remove the entire apm_modules/ directory ({package_count} package(s))")

if not confirm:
_rich_info("Operation cancelled")
return
# Confirmation prompt (skip if --yes provided)
if not yes:
try:
from rich.prompt import Confirm
confirm = Confirm.ask("Continue?")
except ImportError:
confirm = click.confirm("Continue?")

if not confirm:
_rich_info("Operation cancelled")
return

try:
shutil.rmtree(apm_modules_path)
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def _validate_package_exists(package):
"--dry-run", is_flag=True, help="Show what would be installed without installing"
)
@click.option("--force", is_flag=True, help="Overwrite locally-authored files on collision")
@click.option("--verbose", is_flag=True, help="Show detailed installation information")
@click.option("--verbose", "-v", is_flag=True, help="Show detailed installation information")
@click.option(
"--trust-transitive-mcp",
is_flag=True,
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/commands/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def show(ctx, server_name):


@mcp.command(help="List all available MCP servers")
@click.option("--limit", default=20, help="Number of results to show")
@click.option("--limit", default=20, show_default=True, help="Number of results to show")
@click.pass_context
def list(ctx, limit):
"""List all available MCP servers in the registry."""
Expand Down
4 changes: 2 additions & 2 deletions src/apm_cli/commands/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
@click.option(
"--target",
"-t",
type=click.Choice(["copilot", "vscode", "claude", "all"]),
type=click.Choice(["vscode", "copilot", "claude", "all"]),
default=None,
help="Filter files by target (default: auto-detect). 'vscode' is an alias for 'copilot'.",
help="Filter files by target (default: auto-detect). 'copilot' is an alias for 'vscode'.",
)
@click.option("--archive", is_flag=True, default=False, help="Produce a .tar.gz archive.")
@click.option(
Expand Down
4 changes: 2 additions & 2 deletions src/apm_cli/commands/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ def remove(runtime_name):
sys.exit(1)


@runtime.command(help="Check which runtime will be used")
@runtime.command(help="Show active runtime and preference order")
def status():
"""Show which runtime APM will use for execution."""
"""Show active runtime and preference order."""
try:
from ..runtime.manager import RuntimeManager

Expand Down
104 changes: 104 additions & 0 deletions tests/unit/test_deps_clean_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Tests for apm deps clean command --dry-run and --yes flags."""

import contextlib
import os
import tempfile
from pathlib import Path

import pytest
from click.testing import CliRunner

from apm_cli.cli import cli


class TestDepsCleanCommand:
"""Tests for apm deps clean --dry-run and --yes flags."""

def setup_method(self):
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):
try:
os.chdir(self.original_dir)
except (FileNotFoundError, OSError):
repo_root = Path(__file__).parent.parent.parent
os.chdir(str(repo_root))

@contextlib.contextmanager
def _chdir_tmp(self):
"""Create a temp dir, chdir into it, restore CWD on exit."""
with tempfile.TemporaryDirectory() as tmp_dir:
try:
os.chdir(tmp_dir)
yield Path(tmp_dir)
finally:
os.chdir(self.original_dir)

def _create_fake_apm_modules(self, root: Path) -> Path:
"""Create a fake apm_modules/ with one installed package."""
pkg_dir = root / "apm_modules" / "testorg" / "testrepo"
pkg_dir.mkdir(parents=True)
(pkg_dir / "apm.yml").write_text("name: testrepo\n")
return root / "apm_modules"

def test_dry_run_leaves_apm_modules_intact(self):
"""--dry-run must not remove apm_modules/."""
with self._chdir_tmp() as tmp:
apm_modules = self._create_fake_apm_modules(tmp)

result = self.runner.invoke(cli, ["deps", "clean", "--dry-run"])

assert result.exit_code == 0
assert apm_modules.exists(), "apm_modules/ must not be removed in dry-run mode"
assert "Dry run" in result.output

def test_dry_run_lists_packages(self):
"""--dry-run should show the packages that would be removed."""
with self._chdir_tmp() as tmp:
self._create_fake_apm_modules(tmp)

result = self.runner.invoke(cli, ["deps", "clean", "--dry-run"])

assert result.exit_code == 0
assert "testorg/testrepo" in result.output

def test_yes_flag_skips_confirmation(self):
"""--yes must remove apm_modules/ without an interactive prompt."""
with self._chdir_tmp() as tmp:
apm_modules = self._create_fake_apm_modules(tmp)

result = self.runner.invoke(cli, ["deps", "clean", "--yes"])

assert result.exit_code == 0
assert not apm_modules.exists(), "apm_modules/ must be removed when --yes is used"

def test_yes_short_flag_skips_confirmation(self):
"""-y short flag must also skip confirmation."""
with self._chdir_tmp() as tmp:
apm_modules = self._create_fake_apm_modules(tmp)

result = self.runner.invoke(cli, ["deps", "clean", "-y"])

assert result.exit_code == 0
assert not apm_modules.exists()

def test_no_apm_modules_reports_already_clean(self):
"""When apm_modules/ does not exist the command should exit cleanly."""
with self._chdir_tmp():
result = self.runner.invoke(cli, ["deps", "clean"])

assert result.exit_code == 0
assert "already clean" in result.output

def test_dry_run_no_apm_modules_reports_already_clean(self):
"""--dry-run with no apm_modules/ should also exit cleanly."""
with self._chdir_tmp():
result = self.runner.invoke(cli, ["deps", "clean", "--dry-run"])

assert result.exit_code == 0
assert "already clean" in result.output
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.