Skip to content
Merged
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions build/apm.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand All @@ -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**:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
12 changes: 7 additions & 5 deletions src/apm_cli/integration/prompt_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = []
Expand Down
27 changes: 21 additions & 6 deletions src/apm_cli/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@ 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)
# 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:
# Handle PyInstaller bundle vs development
if getattr(sys, 'frozen', False):
Expand All @@ -40,10 +55,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

Expand Down
8 changes: 4 additions & 4 deletions tests/integration/test_auto_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
79 changes: 63 additions & 16 deletions tests/unit/integration/test_prompt_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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('<!--')
assert 'Version: 1.0.0' in content
Expand All @@ -442,7 +442,7 @@ def test_integrate_with_new_version_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",
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = """<!--
Expand All @@ -563,7 +563,7 @@ def test_integrate_mixed_operations(self):
-->

# 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",
Expand Down Expand Up @@ -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)
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.

Loading