From c049fd2c8e154df11b9d6b5450001ef84bf641e0 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 14 Nov 2025 13:28:58 +0000 Subject: [PATCH 1/3] Change integrated prompts to -apm suffix --- CHANGELOG.md | 15 ++++ docs/integrations.md | 20 +++-- pyproject.toml | 2 +- src/apm_cli/cli.py | 2 +- src/apm_cli/integration/prompt_integrator.py | 12 +-- tests/integration/test_auto_integration.py | 8 +- .../integration/test_prompt_integrator.py | 79 +++++++++++++++---- uv.lock | 2 +- 8 files changed, 107 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea88ded7..03b1ce365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.3] - 2025-11-14 + +### Changed +- **Prompt Naming Pattern**: Migrated from `@` prefix to `-apm` suffix for integrated prompts + - New pattern: `accessibility-audit-apm.prompt.md` (was: `@accessibility-audit.prompt.md`) + - Improves VSCode autocomplete discovery - type `/design` to find `design-review-apm.prompt.md` + - Avoids symbol collision with `@` agent syntax in Copilot + - Intent-first naming: search by WHAT (design, compliance) not WHO (apm) +- **GitIgnore Pattern**: Updated from `.github/prompts/@*.prompt.md` to `.github/prompts/*-apm.prompt.md` + +### Migration Notes +- **Existing Users**: Old `@`-prefixed files will not be automatically removed +- **Action Required**: Manually delete old `@*.prompt.md` files from `.github/prompts/` after upgrading +- **Next Install**: Running `apm install --update` will re-sync prompts with new naming pattern + ## [0.5.2] - 2025-11-14 ### Added diff --git a/docs/integrations.md b/docs/integrations.md index c7c3b88df..d7731037e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -144,7 +144,7 @@ APM automatically integrates prompts from installed packages into VSCode's nativ apm install danielmeppiel/design-guidelines # Prompts are automatically integrated to: -# .github/prompts/@*.prompt.md (with package metadata header) +# .github/prompts/*-apm.prompt.md (with package metadata header) ``` **How Auto-Integration Works**: @@ -158,19 +158,29 @@ apm install danielmeppiel/design-guidelines 1. Run `apm install` to fetch APM packages 2. PromptIntegrator checks `auto-integrate` config setting 3. If enabled and `.github/` exists, discovers `.prompt.md` files in each package -4. Copies prompts to `.github/prompts/` with `@` prefix (e.g., `@accessibility-audit.prompt.md`) +4. Copies prompts to `.github/prompts/` with `-apm` suffix (e.g., `accessibility-audit-apm.prompt.md`) 5. Updates `.gitignore` to exclude integrated prompts 6. VSCode automatically loads all prompts for your coding agents +**Intent-First Discovery**: +The `-apm` suffix pattern enables natural autocomplete in VSCode: +- Type `/design` → VSCode shows `design-review-apm.prompt.md` +- Type `/accessibility` → VSCode shows `accessibility-audit-apm.prompt.md` +- Search by what you want to do, not where it comes from + **Example**: ```bash # Install package with auto-integration apm install danielmeppiel/design-guidelines # Result in VSCode: -# .github/prompts/@accessibility-audit.prompt.md ✓ Available in chat -# .github/prompts/@design-review.prompt.md ✓ Available in chat -# .github/prompts/@style-guide-check.prompt.md ✓ Available in chat +# .github/prompts/accessibility-audit-apm.prompt.md ✓ Available in chat +# .github/prompts/design-review-apm.prompt.md ✓ Available in chat +# .github/prompts/style-guide-check-apm.prompt.md ✓ Available in chat + +# Use with natural autocomplete: +# Type: /design +# VSCode suggests: design-review-apm.prompt.md ✨ ``` **VSCode Native Features**: diff --git a/pyproject.toml b/pyproject.toml index 2d927c859..b53535c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "apm-cli" -version = "0.5.2" +version = "0.5.3" description = "MCP configuration tool" readme = "README.md" requires-python = ">=3.9" diff --git a/src/apm_cli/cli.py b/src/apm_cli/cli.py index 77bbdd8a5..5568449a2 100644 --- a/src/apm_cli/cli.py +++ b/src/apm_cli/cli.py @@ -1061,7 +1061,7 @@ def _install_apm_dependencies(apm_package: "APMPackage", update_refs: bool = Fal try: updated = integrator.update_gitignore_for_integrated_prompts(project_root) if updated: - _rich_info("Updated .gitignore for integrated prompts") + _rich_info("Updated .gitignore for integrated prompts (*-apm.prompt.md)") except Exception as e: _rich_warning(f"Could not update .gitignore for prompts: {e}") diff --git a/src/apm_cli/integration/prompt_integrator.py b/src/apm_cli/integration/prompt_integrator.py index 401e30a3a..851182063 100644 --- a/src/apm_cli/integration/prompt_integrator.py +++ b/src/apm_cli/integration/prompt_integrator.py @@ -172,17 +172,19 @@ def generate_header_comment(self, package_info, original_path: Path) -> str: return header def get_target_filename(self, source_file: Path, package_name: str) -> str: - """Generate target filename with @ prefix (simple naming). + """Generate target filename with -apm suffix (intent-first naming). Args: source_file: Source file path package_name: Name of the package (not used in simple naming) Returns: - str: Target filename with @ prefix (e.g., @accessibility-audit.prompt.md) + str: Target filename with -apm suffix (e.g., accessibility-audit-apm.prompt.md) """ - # SIMPLE naming: just prepend @ to the original filename - return f"@{source_file.name}" + # Intent-first naming: insert -apm suffix before .prompt.md extension + # Example: design-review.prompt.md -> design-review-apm.prompt.md + stem = source_file.stem.replace('.prompt', '') # Remove .prompt from stem + return f"{stem}-apm.prompt.md" def copy_prompt_with_header(self, source: Path, target: Path, header: str) -> None: """Copy prompt file with header comment prepended. @@ -283,7 +285,7 @@ def update_gitignore_for_integrated_prompts(self, project_root: Path) -> bool: bool: True if .gitignore was updated, False if pattern already exists """ gitignore_path = project_root / ".gitignore" - pattern = ".github/prompts/@*.prompt.md" + pattern = ".github/prompts/*-apm.prompt.md" # Read current content current_content = [] diff --git a/tests/integration/test_auto_integration.py b/tests/integration/test_auto_integration.py index 9dbc48d34..cf72483c6 100644 --- a/tests/integration/test_auto_integration.py +++ b/tests/integration/test_auto_integration.py @@ -77,13 +77,13 @@ def test_full_integration_workflow(self): # Verify results assert result.files_integrated == 2 - # Check files exist (simple naming with @ prefix) + # Check files exist (intent-first naming with -apm suffix) prompts_dir = self.project_root / ".github" / "prompts" - assert (prompts_dir / "@workflow1.prompt.md").exists() - assert (prompts_dir / "@workflow2.prompt.md").exists() + assert (prompts_dir / "workflow1-apm.prompt.md").exists() + assert (prompts_dir / "workflow2-apm.prompt.md").exists() # Check header comments - content1 = (prompts_dir / "@workflow1.prompt.md").read_text() + content1 = (prompts_dir / "workflow1-apm.prompt.md").read_text() assert "test-package" in content1 assert "abc123def456" in content1 assert "# workflow1" in content1 diff --git a/tests/unit/integration/test_prompt_integrator.py b/tests/unit/integration/test_prompt_integrator.py index 5f2b2e188..128d68c25 100644 --- a/tests/unit/integration/test_prompt_integrator.py +++ b/tests/unit/integration/test_prompt_integrator.py @@ -101,13 +101,13 @@ def test_generate_header_comment(self): assert "test.prompt.md" in header def test_get_target_filename(self): - """Test target filename generation with @ prefix (simple naming).""" + """Test target filename generation with -apm suffix (intent-first naming).""" source = Path("/package/accessibility-audit.prompt.md") package_name = "danielmeppiel/design-guidelines" target = self.integrator.get_target_filename(source, package_name) - # Simple naming: just @ prefix + original filename - assert target == "@accessibility-audit.prompt.md" + # Intent-first naming: -apm suffix before extension + assert target == "accessibility-audit-apm.prompt.md" def test_copy_prompt_with_header(self): """Test copying prompt file with header prepended.""" @@ -177,7 +177,7 @@ def test_integrate_package_prompts_skips_unchanged_files(self): --> # Existing""" - (github_prompts / "@test.prompt.md").write_text(existing_content) + (github_prompts / "test-apm.prompt.md").write_text(existing_content) package = APMPackage( name="test-pkg", @@ -214,12 +214,12 @@ def test_update_gitignore_adds_pattern(self): assert updated == True content = gitignore.read_text() - assert ".github/prompts/@*.prompt.md" in content + assert ".github/prompts/*-apm.prompt.md" in content def test_update_gitignore_skips_if_exists(self): """Test that gitignore update is skipped if pattern exists.""" gitignore = self.project_root / ".gitignore" - gitignore.write_text(".github/prompts/@*.prompt.md\n") + gitignore.write_text(".github/prompts/*-apm.prompt.md\n") updated = self.integrator.update_gitignore_for_integrated_prompts(self.project_root) @@ -416,7 +416,7 @@ def test_integrate_first_time_creates_with_header(self): assert result.files_skipped == 0 # Verify header was added - target_file = github_prompts / "@test.prompt.md" + target_file = github_prompts / "test-apm.prompt.md" content = target_file.read_text() assert content.startswith(' # Old Content""" - (github_prompts / "@test.prompt.md").write_text(old_content) + (github_prompts / "test-apm.prompt.md").write_text(old_content) package = APMPackage( name="test-pkg", @@ -470,7 +470,7 @@ def test_integrate_with_new_version_updates_file(self): assert result.files_skipped == 0 # Verify content was updated - target_file = github_prompts / "@test.prompt.md" + target_file = github_prompts / "test-apm.prompt.md" content = target_file.read_text() assert 'Version: 2.0.0' in content assert '# Updated Content' in content @@ -495,7 +495,7 @@ def test_integrate_with_new_commit_updates_file(self): --> # Old Content""" - (github_prompts / "@test.prompt.md").write_text(old_content) + (github_prompts / "test-apm.prompt.md").write_text(old_content) package = APMPackage( name="test-pkg", @@ -523,7 +523,7 @@ def test_integrate_with_new_commit_updates_file(self): assert result.files_skipped == 0 # Verify commit was updated - target_file = github_prompts / "@test.prompt.md" + target_file = github_prompts / "test-apm.prompt.md" content = target_file.read_text() assert 'Commit: def456' in content assert '# Updated Content' in content @@ -551,7 +551,7 @@ def test_integrate_mixed_operations(self): --> # Old Content""" - (github_prompts / "@update.prompt.md").write_text(update_old) + (github_prompts / "update-apm.prompt.md").write_text(update_old) # Pre-create file to be skipped (same version) skip_same = """ # Unchanged File""" - (github_prompts / "@skip.prompt.md").write_text(skip_same) + (github_prompts / "skip-apm.prompt.md").write_text(skip_same) package = APMPackage( name="test-pkg", @@ -591,12 +591,59 @@ def test_integrate_mixed_operations(self): assert result.files_skipped == 1 # skip.prompt.md # Verify new file exists - assert (github_prompts / "@new.prompt.md").exists() + assert (github_prompts / "new-apm.prompt.md").exists() # Verify updated file has new version - update_content = (github_prompts / "@update.prompt.md").read_text() + update_content = (github_prompts / "update-apm.prompt.md").read_text() assert 'Version: 2.0.0' in update_content # Verify skipped file is unchanged - skip_content = (github_prompts / "@skip.prompt.md").read_text() + skip_content = (github_prompts / "skip-apm.prompt.md").read_text() assert skip_content == skip_same + + +class TestPromptSuffixPattern: + """Test -apm suffix pattern edge cases.""" + + def setup_method(self): + """Set up test fixtures.""" + self.integrator = PromptIntegrator() + + def test_suffix_with_simple_filename(self): + """Test suffix pattern with simple filename.""" + source = Path("test.prompt.md") + result = self.integrator.get_target_filename(source, "pkg") + assert result == "test-apm.prompt.md" + + def test_suffix_with_hyphenated_filename(self): + """Test suffix pattern with hyphenated filename.""" + source = Path("design-review.prompt.md") + result = self.integrator.get_target_filename(source, "pkg") + assert result == "design-review-apm.prompt.md" + + def test_suffix_with_multi_part_filename(self): + """Test suffix pattern with multi-part filename.""" + source = Path("accessibility-audit-wcag.prompt.md") + result = self.integrator.get_target_filename(source, "pkg") + assert result == "accessibility-audit-wcag-apm.prompt.md" + + def test_suffix_preserves_original_name(self): + """Test that original filename structure is preserved.""" + source = Path("my_custom-workflow.prompt.md") + result = self.integrator.get_target_filename(source, "pkg") + assert result == "my_custom-workflow-apm.prompt.md" + + def test_gitignore_pattern_matches_suffix_files(self): + """Test that gitignore pattern matches -apm suffix files.""" + import fnmatch + pattern = "*-apm.prompt.md" + + # Should match + assert fnmatch.fnmatch("design-review-apm.prompt.md", pattern) + assert fnmatch.fnmatch("test-apm.prompt.md", pattern) + assert fnmatch.fnmatch("a-b-c-apm.prompt.md", pattern) + + # Should NOT match + assert not fnmatch.fnmatch("design-review.prompt.md", pattern) + assert not fnmatch.fnmatch("apm.prompt.md", pattern) + assert not fnmatch.fnmatch("@design-review.prompt.md", pattern) diff --git a/uv.lock b/uv.lock index f4c3b27ab..4e0a711ef 100644 --- a/uv.lock +++ b/uv.lock @@ -166,7 +166,7 @@ wheels = [ [[package]] name = "apm-cli" -version = "0.5.1" +version = "0.5.2" source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, From 2d1b5a83a5be6589b2e5b6d38b5c8fb0aeaaf1ba Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 14 Nov 2025 15:06:13 +0000 Subject: [PATCH 2/3] Prefer importlib.metadata in get_version; fallback to pyproject.toml and clarify PyInstaller handling --- src/apm_cli/version.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/apm_cli/version.py b/src/apm_cli/version.py index 78e00e25f..25fc7bb19 100644 --- a/src/apm_cli/version.py +++ b/src/apm_cli/version.py @@ -12,16 +12,29 @@ def get_version() -> str: """ Get the current version efficiently. - First tries build-time constant, then falls back to pyproject.toml parsing. + First tries build-time constant, then installed package metadata, + then falls back to pyproject.toml parsing (for development). Returns: str: Version string """ - # Use build-time constant if available (fastest path) + # Use build-time constant if available (fastest path - for PyInstaller binaries) if __BUILD_VERSION__: return __BUILD_VERSION__ - # Fallback to reading from pyproject.toml (for development) + # Try to get version from installed package metadata (for pip installations) + try: + # Python 3.8+ has importlib.metadata + if sys.version_info >= (3, 8): + from importlib.metadata import version, PackageNotFoundError + else: + from importlib_metadata import version, PackageNotFoundError + + return version("apm-cli") + except (ImportError, PackageNotFoundError): + pass + + # Fallback to reading from pyproject.toml (for development/source installations) try: # Handle PyInstaller bundle vs development if getattr(sys, 'frozen', False): @@ -40,10 +53,10 @@ def get_version() -> str: import re match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content) if match: - version = match.group(1) + version_str = match.group(1) # Validate PEP 440 version patterns: x.y.z or x.y.z{a|b|rc}N - if re.match(r'^\d+\.\d+\.\d+(a\d+|b\d+|rc\d+)?$', version): - return version + if re.match(r'^\d+\.\d+\.\d+(a\d+|b\d+|rc\d+)?$', version_str): + return version_str except Exception: pass From 1469cebc9edcf4d6b9d2ffa1e14c4fb8716b9070 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 14 Nov 2025 20:03:56 +0000 Subject: [PATCH 3/3] Skip importlib.metadata lookup in frozen/PyInstaller; preserve distutils in spec; bump apm-cli to 0.5.3 --- build/apm.spec | 6 ++++-- src/apm_cli/version.py | 22 ++++++++++++---------- uv.lock | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/build/apm.spec b/build/apm.spec index fad81796a..d6501f8c5 100644 --- a/build/apm.spec +++ b/build/apm.spec @@ -145,6 +145,9 @@ hiddenimports = [ # Subprocess for runtime operations 'subprocess', 'shlex', + # Version detection for pip installations + 'importlib.metadata', + 'importlib_metadata', ] # Modules to exclude to reduce binary size @@ -172,8 +175,7 @@ excludes = [ 'bdb', 'test', 'tests', - # Build tools - not needed at runtime - 'distutils', + # Build tools - not needed at runtime (but keep distutils as it's needed by importlib.metadata) 'lib2to3', # Audio/image processing - not needed 'wave', # safe to exclude diff --git a/src/apm_cli/version.py b/src/apm_cli/version.py index 25fc7bb19..54f98176d 100644 --- a/src/apm_cli/version.py +++ b/src/apm_cli/version.py @@ -23,16 +23,18 @@ def get_version() -> str: return __BUILD_VERSION__ # Try to get version from installed package metadata (for pip installations) - try: - # Python 3.8+ has importlib.metadata - if sys.version_info >= (3, 8): - from importlib.metadata import version, PackageNotFoundError - else: - from importlib_metadata import version, PackageNotFoundError - - return version("apm-cli") - except (ImportError, PackageNotFoundError): - pass + # Skip this in frozen/PyInstaller environments to avoid import issues + if not getattr(sys, 'frozen', False): + try: + # Python 3.8+ has importlib.metadata + if sys.version_info >= (3, 8): + from importlib.metadata import version, PackageNotFoundError + else: + from importlib_metadata import version, PackageNotFoundError + + return version("apm-cli") + except (ImportError, PackageNotFoundError): + pass # Fallback to reading from pyproject.toml (for development/source installations) try: diff --git a/uv.lock b/uv.lock index 4e0a711ef..56b42a72f 100644 --- a/uv.lock +++ b/uv.lock @@ -166,7 +166,7 @@ wheels = [ [[package]] name = "apm-cli" -version = "0.5.2" +version = "0.5.3" source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },