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
7 changes: 5 additions & 2 deletions src/apm_cli/commands/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ def _recompile(self, changed_file):
@click.option(
"--target",
"-t",
type=click.Choice(["vscode", "agents", "claude", "all"]),
type=click.Choice(["vscode", "agents", "claude", "opencode", "all"]),
default=None,
help="Target platform: vscode/agents (AGENTS.md), claude (CLAUDE.md), or all. Auto-detects if not specified.",
help="Target platform: vscode/agents (AGENTS.md), opencode (AGENTS.md + .opencode/), claude (CLAUDE.md), or all. Auto-detects if not specified.",
)
@click.option(
"--dry-run",
Expand Down Expand Up @@ -316,6 +316,7 @@ def compile(

Target platforms:
* vscode/agents: Generates AGENTS.md + .github/ structure (VSCode/GitHub Copilot)
* opencode: Generates AGENTS.md + .opencode/ structure (OpenCode)
* claude: Generates CLAUDE.md + .claude/ structure (Claude Code)
* all: Generates both targets (default)

Expand Down Expand Up @@ -467,6 +468,8 @@ def compile(
_rich_info(
f"Compiling for AGENTS.md (VSCode/Copilot) - {detection_reason}"
)
elif detected_target == "opencode":
_rich_info(f"Compiling for AGENTS.md (OpenCode) - {detection_reason}")
elif detected_target == "claude":
_rich_info(
f"Compiling for CLAUDE.md (Claude Code) - {detection_reason}"
Expand Down
133 changes: 126 additions & 7 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,7 @@ def _collect_descendants(node, visited=None):
detect_target,
should_integrate_vscode,
should_integrate_claude,
should_integrate_opencode,
get_target_description,
)

Expand All @@ -888,10 +889,11 @@ def _collect_descendants(node, visited=None):
# Per skill-strategy Decision 1, .github/skills/ is the standard skills location;
# creating .github/ here ensures a consistent skills root and also enables
# VSCode/Copilot integration by default (quick path to value), even for
# projects that don't yet use .claude/.
# projects that don't yet use .claude/ or .opencode/.
github_dir = project_root / ".github"
claude_dir = project_root / ".claude"
if not github_dir.exists() and not claude_dir.exists():
opencode_dir = project_root / ".opencode"
if not github_dir.exists() and not claude_dir.exists() and not opencode_dir.exists():
github_dir.mkdir(parents=True, exist_ok=True)
_rich_info(
"Created .github/ as standard skills root (.github/skills/) and to enable VSCode/Copilot integration"
Expand All @@ -906,6 +908,7 @@ def _collect_descendants(node, visited=None):
# 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()
Expand Down Expand Up @@ -1232,7 +1235,7 @@ def _collect_descendants(node, visited=None):
installed_count += 1

# Still need to integrate prompts for cached packages (zero-config behavior)
if integrate_vscode or integrate_claude:
if integrate_vscode or integrate_claude or integrate_opencode:
try:
# Create PackageInfo from cached package
from apm_cli.models.apm_package import (
Expand Down Expand Up @@ -1312,7 +1315,7 @@ def _collect_descendants(node, visited=None):
package_types[dep_key] = cached_package_info.package_type.value

# VSCode + Claude integration (prompts + agents)
if integrate_vscode or integrate_claude:
if integrate_vscode or integrate_claude or integrate_opencode:
# Integrate prompts
prompt_result = (
prompt_integrator.integrate_package_prompts(
Expand Down Expand Up @@ -1363,7 +1366,7 @@ def _collect_descendants(node, visited=None):

# Skill integration (works for both VSCode and Claude)
# Skills go to .github/skills/ (primary) and .claude/skills/ (if .claude/ exists)
if integrate_vscode or integrate_claude:
if integrate_vscode or integrate_claude or integrate_opencode:
skill_result = skill_integrator.integrate_package_skill(
cached_package_info, project_root
)
Expand All @@ -1380,6 +1383,28 @@ def _collect_descendants(node, visited=None):
for tp in skill_result.target_paths:
dep_deployed.append(tp.relative_to(project_root).as_posix())

if integrate_opencode:
skill_result_opencode = (
skill_integrator.integrate_package_skill_opencode(
cached_package_info, project_root,
diagnostics=diagnostics,
)
)
if skill_result_opencode.skill_created:
total_skills_integrated += 1
_rich_info(
" └─ Skill integrated → .opencode/skills/"
)
if skill_result_opencode.sub_skills_promoted > 0:
total_sub_skills_promoted += (
skill_result_opencode.sub_skills_promoted
)
_rich_info(
f" └─ {skill_result_opencode.sub_skills_promoted} skill(s) integrated → .opencode/skills/"
)
for tp in skill_result_opencode.target_paths:
dep_deployed.append(tp.relative_to(project_root).as_posix())

# Integrate instructions → .github/instructions/
if integrate_vscode:
instruction_result = (
Expand Down Expand Up @@ -1444,6 +1469,42 @@ def _collect_descendants(node, visited=None):
for tp in command_result.target_paths:
dep_deployed.append(tp.relative_to(project_root).as_posix())

# OpenCode-specific integration (agents + commands)
if integrate_opencode:
opencode_agent_result = (
agent_integrator.integrate_package_agents_opencode(
cached_package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
)
if opencode_agent_result.files_integrated > 0:
total_agents_integrated += (
opencode_agent_result.files_integrated
)
_rich_info(
f" └─ {opencode_agent_result.files_integrated} agents integrated → .opencode/agents/"
)
total_links_resolved += opencode_agent_result.links_resolved
for tp in opencode_agent_result.target_paths:
dep_deployed.append(tp.relative_to(project_root).as_posix())

opencode_command_result = command_integrator.integrate_package_commands_opencode(
cached_package_info, project_root,
force=force, managed_files=managed_files,
diagnostics=diagnostics,
)
if opencode_command_result.files_integrated > 0:
total_commands_integrated += (
opencode_command_result.files_integrated
)
_rich_info(
f" └─ {opencode_command_result.files_integrated} commands integrated → .opencode/commands/"
)
total_links_resolved += opencode_command_result.links_resolved
for tp in opencode_command_result.target_paths:
dep_deployed.append(tp.relative_to(project_root).as_posix())

# Hook integration (target-aware)
if integrate_vscode:
hook_result = hook_integrator.integrate_package_hooks(
Expand Down Expand Up @@ -1562,7 +1623,7 @@ def _collect_descendants(node, visited=None):
_rich_info(f" └─ Package type: APM Package (apm.yml)")

# Auto-integrate prompts and agents if enabled
if integrate_vscode or integrate_claude:
if integrate_vscode or integrate_claude or integrate_opencode:
try:
# Integrate prompts + agents (dual-target: .github/ + .claude/)
# Integrate prompts
Expand Down Expand Up @@ -1615,7 +1676,7 @@ def _collect_descendants(node, visited=None):

# Skill integration (works for both VSCode and Claude)
# Skills go to .github/skills/ (primary) and .claude/skills/ (if .claude/ exists)
if integrate_vscode or integrate_claude:
if integrate_vscode or integrate_claude or integrate_opencode:
skill_result = skill_integrator.integrate_package_skill(
package_info, project_root
)
Expand All @@ -1632,6 +1693,28 @@ def _collect_descendants(node, visited=None):
for tp in skill_result.target_paths:
dep_deployed_fresh.append(tp.relative_to(project_root).as_posix())

if integrate_opencode:
skill_result_opencode = (
skill_integrator.integrate_package_skill_opencode(
package_info, project_root,
diagnostics=diagnostics,
)
)
if skill_result_opencode.skill_created:
total_skills_integrated += 1
_rich_info(
" └─ Skill integrated → .opencode/skills/"
)
if skill_result_opencode.sub_skills_promoted > 0:
total_sub_skills_promoted += (
skill_result_opencode.sub_skills_promoted
)
_rich_info(
f" └─ {skill_result_opencode.sub_skills_promoted} skill(s) integrated → .opencode/skills/"
)
for tp in skill_result_opencode.target_paths:
dep_deployed_fresh.append(tp.relative_to(project_root).as_posix())

# Integrate instructions → .github/instructions/
if integrate_vscode:
instruction_result = (
Expand Down Expand Up @@ -1696,6 +1779,42 @@ def _collect_descendants(node, visited=None):
for tp in command_result.target_paths:
dep_deployed_fresh.append(tp.relative_to(project_root).as_posix())

# OpenCode-specific integration (agents + commands)
if integrate_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:
total_agents_integrated += (
opencode_agent_result.files_integrated
)
_rich_info(
f" └─ {opencode_agent_result.files_integrated} agents integrated → .opencode/agents/"
)
total_links_resolved += opencode_agent_result.links_resolved
for tp in opencode_agent_result.target_paths:
dep_deployed_fresh.append(tp.relative_to(project_root).as_posix())

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:
total_commands_integrated += (
opencode_command_result.files_integrated
)
_rich_info(
f" └─ {opencode_command_result.files_integrated} commands integrated → .opencode/commands/"
)
total_links_resolved += opencode_command_result.links_resolved
for tp in opencode_command_result.target_paths:
dep_deployed_fresh.append(tp.relative_to(project_root).as_posix())

# Hook integration (target-aware)
if integrate_vscode:
hook_result = hook_integrator.integrate_package_hooks(
Expand Down
7 changes: 7 additions & 0 deletions src/apm_cli/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ def _find_transitive_orphans(lockfile, removed_urls):
from ..core.target_detection import (
detect_target,
should_integrate_claude,
should_integrate_opencode,
)
config_target = apm_package.target
detected_target, _ = detect_target(
Expand All @@ -454,6 +455,7 @@ def _find_transitive_orphans(lockfile, removed_urls):
config_target=config_target,
)
integrate_claude = should_integrate_claude(detected_target)
integrate_opencode = should_integrate_opencode(detected_target)

prompt_integrator = PromptIntegrator()
agent_integrator = AgentIntegrator()
Expand Down Expand Up @@ -489,9 +491,14 @@ def _find_transitive_orphans(lockfile, removed_urls):
agent_integrator.integrate_package_agents(pkg_info, project_root)
if integrate_claude:
agent_integrator.integrate_package_agents_claude(pkg_info, project_root)
if integrate_opencode:
agent_integrator.integrate_package_agents_opencode(pkg_info, project_root)
skill_integrator.integrate_package_skill(pkg_info, project_root)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During uninstall re-integration, integrate_package_skill() is invoked unconditionally, which will recreate .github/skills/ (and possibly .claude/skills/) even when the resolved target is opencode. If OpenCode is intended to be a standalone target, consider gating .github/.claude skill reintegration on integrate_vscode/integrate_claude and using integrate_package_skill_opencode() for the OpenCode-only path.

Suggested change
skill_integrator.integrate_package_skill(pkg_info, project_root)
if integrate_vscode or integrate_claude:
skill_integrator.integrate_package_skill(
pkg_info, project_root
)

Copilot uses AI. Check for mistakes.
if integrate_claude:
command_integrator.integrate_package_commands(pkg_info, project_root)
if integrate_opencode:
command_integrator.integrate_package_commands_opencode(pkg_info, project_root)
skill_integrator.integrate_package_skill_opencode(pkg_info, project_root)
hook_integrator_reint.integrate_package_hooks(pkg_info, project_root)
if integrate_claude:
hook_integrator_reint.integrate_package_hooks_claude(pkg_info, project_root)
Expand Down
4 changes: 2 additions & 2 deletions src/apm_cli/compilation/agents_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ def compile(self, config: CompilationConfig, primitives: Optional[PrimitiveColle
# Route to targets based on config.target
results: List[CompilationResult] = []

# AGENTS.md target (vscode/agents)
if config.target in ("vscode", "agents", "all"):
# AGENTS.md target (vscode/agents/opencode)
if config.target in ("vscode", "agents", "opencode", "all"):
results.append(self._compile_agents_md(config, primitives))

# CLAUDE.md target
Expand Down
Loading