Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `--target opencode` no longer creates `.github/` directory -- target-driven dispatch via `INTEGRATION_DISPATCH` registry replaces per-target boolean flags (#470)

### Changed

- Refactored `_integrate_package_primitives()` to use declarative `INTEGRATION_DISPATCH` registry in `targets.py` instead of monolithic if-chains (#470)

## [0.8.6] - 2026-03-27

### Added
Expand Down
253 changes: 28 additions & 225 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,9 +837,7 @@ def _integrate_package_primitives(
package_info,
project_root,
*,
integrate_vscode,
integrate_claude,
integrate_opencode=False,
targets,
prompt_integrator,
agent_integrator,
skill_integrator,
Expand All @@ -854,215 +852,34 @@ def _integrate_package_primitives(
):
"""Run the full integration pipeline for a single package.

Delegates to ``integrate_package_for_targets()`` which iterates the
active ``TargetProfile`` list and dispatches to the correct integrator
method for each supported primitive. This function is a thin adapter
that keeps the call-site signature stable.

Returns a dict with integration counters and the list of deployed file paths.
"""
result = {
"prompts": 0,
"agents": 0,
"skills": 0,
"sub_skills": 0,
"instructions": 0,
"commands": 0,
"hooks": 0,
"links_resolved": 0,
"deployed_files": [],
from apm_cli.integration.targets import integrate_package_for_targets

integrators = {
"prompt_integrator": prompt_integrator,
"agent_integrator": agent_integrator,
"skill_integrator": skill_integrator,
"instruction_integrator": instruction_integrator,
"command_integrator": command_integrator,
"hook_integrator": hook_integrator,
}

deployed = result["deployed_files"]

if not (integrate_vscode or integrate_claude or integrate_opencode):
return result

def _log_integration(msg):
if logger:
logger.tree_item(msg)

# --- prompts ---
prompt_result = prompt_integrator.integrate_package_prompts(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if prompt_result.files_integrated > 0:
result["prompts"] += prompt_result.files_integrated
_log_integration(f" └─ {prompt_result.files_integrated} prompts integrated -> .github/prompts/")
if prompt_result.files_updated > 0:
_log_integration(f" └─ {prompt_result.files_updated} prompts updated")
result["links_resolved"] += prompt_result.links_resolved
for tp in prompt_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- agents (.github) ---
agent_result = agent_integrator.integrate_package_agents(
package_info, project_root,
force=force, managed_files=managed_files,
return integrate_package_for_targets(
targets,
package_info,
project_root,
integrators,
force=force,
managed_files=managed_files,
diagnostics=diagnostics,
logger=logger,
)
if agent_result.files_integrated > 0:
result["agents"] += agent_result.files_integrated
_log_integration(f" └─ {agent_result.files_integrated} agents integrated -> .github/agents/")
if agent_result.files_updated > 0:
_log_integration(f" └─ {agent_result.files_updated} agents updated")
result["links_resolved"] += agent_result.links_resolved
for tp in agent_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- skills ---
if integrate_vscode or integrate_claude or integrate_opencode:
skill_result = skill_integrator.integrate_package_skill(
package_info, project_root,
diagnostics=diagnostics, managed_files=managed_files, force=force,
)
# Build human-readable list of target dirs from deployed paths
_skill_target_dirs: set[str] = set()
for tp in skill_result.target_paths:
rel = tp.relative_to(project_root)
if rel.parts:
_skill_target_dirs.add(rel.parts[0])
_skill_targets = sorted(_skill_target_dirs)
_skill_target_str = ", ".join(f"{d}/skills/" for d in _skill_targets) or "skills/"
if skill_result.skill_created:
result["skills"] += 1
_log_integration(f" |-- Skill integrated -> {_skill_target_str}")
if skill_result.sub_skills_promoted > 0:
result["sub_skills"] += skill_result.sub_skills_promoted
_log_integration(f" |-- {skill_result.sub_skills_promoted} skill(s) integrated -> {_skill_target_str}")
for tp in skill_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- instructions (.github) ---
if integrate_vscode:
instruction_result = instruction_integrator.integrate_package_instructions(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if instruction_result.files_integrated > 0:
result["instructions"] += instruction_result.files_integrated
_log_integration(f" └─ {instruction_result.files_integrated} instruction(s) integrated -> .github/instructions/")
result["links_resolved"] += instruction_result.links_resolved
for tp in instruction_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- Cursor rules (.cursor/rules/) ---
cursor_rules_result = instruction_integrator.integrate_package_instructions_cursor(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if cursor_rules_result.files_integrated > 0:
result["instructions"] += cursor_rules_result.files_integrated
_log_integration(f" └─ {cursor_rules_result.files_integrated} rule(s) integrated -> .cursor/rules/")
result["links_resolved"] += cursor_rules_result.links_resolved
for tp in cursor_rules_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- Claude agents (.claude) ---
if integrate_claude:
claude_agent_result = agent_integrator.integrate_package_agents_claude(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if claude_agent_result.files_integrated > 0:
result["agents"] += claude_agent_result.files_integrated
_log_integration(f" └─ {claude_agent_result.files_integrated} agents integrated -> .claude/agents/")
result["links_resolved"] += claude_agent_result.links_resolved
for tp in claude_agent_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- Cursor agents (.cursor) ---
cursor_agent_result = agent_integrator.integrate_package_agents_cursor(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if cursor_agent_result.files_integrated > 0:
result["agents"] += cursor_agent_result.files_integrated
_log_integration(f" └─ {cursor_agent_result.files_integrated} agents integrated -> .cursor/agents/")
result["links_resolved"] += cursor_agent_result.links_resolved
for tp in cursor_agent_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- OpenCode agents (.opencode) ---
opencode_agent_result = agent_integrator.integrate_package_agents_opencode(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if opencode_agent_result.files_integrated > 0:
result["agents"] += opencode_agent_result.files_integrated
_log_integration(f" └─ {opencode_agent_result.files_integrated} agents integrated -> .opencode/agents/")
result["links_resolved"] += opencode_agent_result.links_resolved
for tp in opencode_agent_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- commands (.claude) ---
if integrate_claude:
command_result = command_integrator.integrate_package_commands(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if command_result.files_integrated > 0:
result["commands"] += command_result.files_integrated
_log_integration(f" └─ {command_result.files_integrated} commands integrated -> .claude/commands/")
if command_result.files_updated > 0:
_log_integration(f" └─ {command_result.files_updated} commands updated")
result["links_resolved"] += command_result.links_resolved
for tp in command_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- OpenCode commands (.opencode) ---
opencode_command_result = command_integrator.integrate_package_commands_opencode(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if opencode_command_result.files_integrated > 0:
result["commands"] += opencode_command_result.files_integrated
_log_integration(f" └─ {opencode_command_result.files_integrated} commands integrated -> .opencode/commands/")
result["links_resolved"] += opencode_command_result.links_resolved
for tp in opencode_command_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# --- hooks ---
if integrate_vscode:
hook_result = hook_integrator.integrate_package_hooks(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if hook_result.hooks_integrated > 0:
result["hooks"] += hook_result.hooks_integrated
_log_integration(f" └─ {hook_result.hooks_integrated} hook(s) integrated -> .github/hooks/")
for tp in hook_result.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())
if integrate_claude:
hook_result_claude = hook_integrator.integrate_package_hooks_claude(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if hook_result_claude.hooks_integrated > 0:
result["hooks"] += hook_result_claude.hooks_integrated
_log_integration(f" └─ {hook_result_claude.hooks_integrated} hook(s) integrated -> .claude/settings.json")
for tp in hook_result_claude.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

# Cursor hooks (.cursor/hooks.json) — method self-guards on .cursor/ existence
hook_result_cursor = hook_integrator.integrate_package_hooks_cursor(
package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if hook_result_cursor.hooks_integrated > 0:
result["hooks"] += hook_result_cursor.hooks_integrated
_log_integration(f" └─ {hook_result_cursor.hooks_integrated} hook(s) integrated -> .cursor/hooks.json")
for tp in hook_result_cursor.target_paths:
deployed.append(tp.relative_to(project_root).as_posix())

return result



Expand Down Expand Up @@ -1349,9 +1166,6 @@ def _collect_descendants(node, visited=None):
# Auto-detect target for integration (same logic as compile)
from apm_cli.core.target_detection import (
detect_target,
should_integrate_vscode,
should_integrate_claude,
should_integrate_opencode,
get_target_description,
)

Expand Down Expand Up @@ -1382,11 +1196,6 @@ def _collect_descendants(node, visited=None):
config_target=config_target,
)

# Determine which integrations to run based on detected target
integrate_vscode = should_integrate_vscode(detected_target)
integrate_claude = should_integrate_claude(detected_target)
integrate_opencode = should_integrate_opencode(detected_target)

# Initialize integrators
prompt_integrator = PromptIntegrator()
agent_integrator = AgentIntegrator()
Expand Down Expand Up @@ -1647,9 +1456,7 @@ def _collect_descendants(node, visited=None):

int_result = _integrate_package_primitives(
package_info, project_root,
integrate_vscode=integrate_vscode,
integrate_claude=integrate_claude,
integrate_opencode=integrate_opencode,
targets=_targets,
prompt_integrator=prompt_integrator,
agent_integrator=agent_integrator,
skill_integrator=skill_integrator,
Expand Down Expand Up @@ -1766,7 +1573,7 @@ def _collect_descendants(node, visited=None):
unpinned_count += 1

# Skip integration if not needed
if not (integrate_vscode or integrate_claude or integrate_opencode):
if not _targets:
continue

# Integrate prompts for cached packages (zero-config behavior)
Expand Down Expand Up @@ -1851,9 +1658,7 @@ def _collect_descendants(node, visited=None):

int_result = _integrate_package_primitives(
cached_package_info, project_root,
integrate_vscode=integrate_vscode,
integrate_claude=integrate_claude,
integrate_opencode=integrate_opencode,
targets=_targets,
prompt_integrator=prompt_integrator,
agent_integrator=agent_integrator,
skill_integrator=skill_integrator,
Expand Down Expand Up @@ -2013,13 +1818,11 @@ def _collect_descendants(node, visited=None):
package_deployed_files[dep_ref.get_unique_key()] = []
continue

if integrate_vscode or integrate_claude or integrate_opencode:
if _targets:
try:
int_result = _integrate_package_primitives(
package_info, project_root,
integrate_vscode=integrate_vscode,
integrate_claude=integrate_claude,
integrate_opencode=integrate_opencode,
targets=_targets,
prompt_integrator=prompt_integrator,
agent_integrator=agent_integrator,
skill_integrator=skill_integrator,
Expand Down
4 changes: 4 additions & 0 deletions src/apm_cli/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
TargetProfile,
PrimitiveMapping,
KNOWN_TARGETS,
INTEGRATION_DISPATCH,
get_integration_prefixes,
active_targets,
integrate_package_for_targets,
)

__all__ = [
Expand All @@ -38,8 +40,10 @@
'TargetProfile',
'PrimitiveMapping',
'KNOWN_TARGETS',
'INTEGRATION_DISPATCH',
'get_integration_prefixes',
'active_targets',
'integrate_package_for_targets',
'validate_skill_name',
'normalize_skill_name',
'to_hyphen_case',
Expand Down
Loading