Skip to content

feat: add standalone OpenCode target integration#257

Closed
timvw wants to merge 1 commit intomicrosoft:mainfrom
timvw:feat/opencode-target-standalone-v2
Closed

feat: add standalone OpenCode target integration#257
timvw wants to merge 1 commit intomicrosoft:mainfrom
timvw:feat/opencode-target-standalone-v2

Conversation

@timvw
Copy link
Contributor

@timvw timvw commented Mar 12, 2026

Summary

  • add opencode as a standalone target in target detection and compile routing (including --target opencode)
  • integrate package agents, commands, and skills into project-local .opencode/agents, .opencode/commands, and .opencode/skills
  • wire install/uninstall orchestration and managed-file cleanup for OpenCode target flows in the refactored command modules (commands/install.py, commands/uninstall.py)
  • extend unit coverage for target detection, compile target flags, and OpenCode integrator behavior

Test Plan

  • uv run --extra dev pytest tests/unit/core/test_target_detection.py tests/unit/compilation/test_compile_target_flag.py tests/unit/integration/test_agent_integrator.py tests/unit/integration/test_command_integrator.py tests/unit/integration/test_skill_integrator.py -q
  • uv run --extra dev pytest tests/unit -q
  • uv run --extra dev pytest tests/unit/core/test_target_detection.py tests/unit/test_install_command.py tests/unit/test_uninstall_transitive_cleanup.py tests/unit/compilation/test_compilation.py -q

Notes

  • this PR supersedes feat: add OpenCode target support for install and integration #170 with a clean branch refresh and isolated OpenCode-focused diff
  • the upstream CLI refactor moved logic from cli.py into command modules; OpenCode install/uninstall integration was ported to those modules
  • attempted full pytest -q/integration runs were interrupted by the tool session before completion, so unit and targeted regression coverage is provided above

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds opencode as a first-class/standalone target across target auto-detection, compile routing, and install/uninstall integration so APM can deploy agents/commands/skills under .opencode/ when requested.

Changes:

  • Extend target detection to support opencode (explicit/config/auto-detect), and update compile routing so --target opencode generates AGENTS.md.
  • Add OpenCode-specific integration flows for agents, commands, and skills under .opencode/, plus managed-file partitioning/cleanup support.
  • Add/extend unit tests covering opencode target detection, CLI target flag acceptance, and OpenCode integrator behaviors.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/apm_cli/core/target_detection.py Adds opencode to target types, auto-detection, and target descriptions.
src/apm_cli/commands/compile.py Allows --target opencode and updates help/output messaging.
src/apm_cli/compilation/agents_compiler.py Routes compilation so opencode runs AGENTS.md generation.
src/apm_cli/integration/base_integrator.py Adds .opencode/ to allowed integration prefixes and partitions managed files into new OpenCode buckets.
src/apm_cli/integration/agent_integrator.py Adds .opencode/agents integration + OpenCode sync cleanup.
src/apm_cli/integration/command_integrator.py Adds .opencode/commands integration + OpenCode sync cleanup.
src/apm_cli/integration/skill_integrator.py Adds .opencode/skills copy/promotion + OpenCode-only skill integration method + cleanup support.
src/apm_cli/commands/install.py Wires OpenCode integration + deployed-files tracking for install flows (cached and fresh).
src/apm_cli/commands/uninstall.py Wires OpenCode cleanup and re-integration orchestration during uninstall.
tests/unit/core/test_target_detection.py Adds tests for opencode detection + compile/integrate gating helpers.
tests/unit/compilation/test_compile_target_flag.py Adds coverage ensuring --target opencode is accepted and routes to AGENTS.md generation.
tests/unit/integration/test_agent_integrator.py Adds .opencode/agents integration + sync cleanup tests.
tests/unit/integration/test_command_integrator.py Adds .opencode/commands integration + sync cleanup tests.
docs/plans/2026-03-11-opencode-target-standalone.md Implementation plan document for the OpenCode standalone target.
Comments suppressed due to low confidence (2)

src/apm_cli/commands/install.py:1530

  • In the fresh-download integration path, integrate_package_skill_opencode() is invoked before .opencode/ is created (OpenCode agents/commands integration happens later). Because the method skips when .opencode/ doesn’t exist, OpenCode skills won’t be installed unless .opencode/ already exists. Ensure .opencode/ is created before OpenCode skill integration, or have the skill integrator create its target root automatically.
                                if integrate_opencode:
                                    skill_result_opencode = skill_integrator.integrate_package_skill_opencode(
                                        package_info, project_root
                                    )

src/apm_cli/commands/install.py:1130

  • integrate_opencode is included in the condition that calls integrate_package_skill(), which deploys skills to .github/skills/ (and .claude/skills/ when present). If --target opencode is meant to be a standalone target, this will still write VSCode/Claude skill artifacts even when VSCode/Claude integration is disabled. Consider gating integrate_package_skill() on integrate_vscode or integrate_claude only, and using integrate_package_skill_opencode() for the OpenCode-only path (or making the skill integrator target-aware).
                            if (
                                integrate_vscode
                                or integrate_claude
                                or integrate_opencode
                            ):

Comment on lines +1152 to +1159
skill_result_opencode = skill_integrator.integrate_package_skill_opencode(
cached_package_info, project_root
)
if skill_result_opencode.skill_created:
total_skills_integrated += 1
_rich_info(
" └─ Skill integrated → .opencode/skills/"
)
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.

integrate_package_skill_opencode() is called before any OpenCode integrator has created .opencode/ (the .opencode directory is only created later when agents/commands are integrated). Since integrate_package_skill_opencode() returns early when .opencode/ doesn’t exist, OpenCode skills will be silently skipped on a fresh project unless the user pre-creates .opencode/. Fix by ensuring .opencode/ is created up-front when integrate_opencode is enabled, or by having integrate_package_skill_opencode() create its target root directory.

This issue also appears on line 1527 of the same file.

Suggested change
skill_result_opencode = skill_integrator.integrate_package_skill_opencode(
cached_package_info, project_root
)
if skill_result_opencode.skill_created:
total_skills_integrated += 1
_rich_info(
" └─ Skill integrated → .opencode/skills/"
)
# Ensure .opencode/ exists before integrating OpenCode skills.
(project_root / ".opencode").mkdir(
parents=True, exist_ok=True
)
skill_result_opencode = skill_integrator.integrate_package_skill_opencode(
cached_package_info, project_root
)
if skill_result_opencode.skill_created:
total_skills_integrated += 1
_rich_info(
" └─ Skill integrated → .opencode/skills/"
)

Copilot uses AI. Check for mistakes.
Comment on lines +1502 to +1506
if (
integrate_vscode
or integrate_claude
or integrate_opencode
):
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.

Same issue as the cached-package path: integrate_opencode currently triggers integrate_package_skill(), which deploys into .github/skills/ (and .claude/skills/). For a standalone OpenCode target this will create non-OpenCode artifacts even when VSCode/Claude integrations are otherwise off. Consider splitting the gating so OpenCode only calls integrate_package_skill_opencode() (or otherwise prevents .github/skills/ writes when target is opencode).

This issue also appears on line 1126 of the same file.

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 102
# Known integration prefixes that APM is allowed to deploy/remove under
INTEGRATION_PREFIXES = (".github/", ".claude/")
INTEGRATION_PREFIXES = (".github/", ".claude/", ".opencode/")

@staticmethod
def validate_deploy_path(
rel_path: str,
project_root: Path,
allowed_prefixes: tuple = (".github/", ".claude/"),
allowed_prefixes: tuple = INTEGRATION_PREFIXES,
) -> bool:
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.

INTEGRATION_PREFIXES now allows .opencode/ deploy/removal. This changes APM’s documented security boundary (allowed prefixes) and target integrations; several Starlight docs still state only .github/ and .claude/ are allowed. Please update the relevant docs pages (e.g., Security Model, Compilation/CLI target docs, pack filtering, manifest schema) to include .opencode/ and the new opencode target.

Copilot uses AI. Check for mistakes.
Comment on lines 171 to +176
descriptions = {
"vscode": "AGENTS.md + .github/prompts/ + .github/agents/",
"claude": "CLAUDE.md + .claude/commands/ + .claude/agents/ + .claude/skills/",
"all": "AGENTS.md + CLAUDE.md + .github/ + .claude/",
"minimal": "AGENTS.md only (create .github/ or .claude/ for full integration)",
"opencode": "AGENTS.md + .opencode/agents/ + .opencode/commands/ + .opencode/skills/",
"all": "AGENTS.md + CLAUDE.md + .github/ + .claude/ + .opencode/",
"minimal": "AGENTS.md only (create .github/, .claude/, or .opencode/ for full integration)",
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.

opencode is now a supported target in detection and descriptions, but the Starlight documentation still only lists vscode/agents, claude, all, and minimal in multiple places (compile guide, CLI reference, manifest schema, pack target filtering, etc.). Please update docs/src/content/docs to document the opencode target behavior, auto-detection rules, and the .opencode/ integration paths.

Copilot uses AI. Check for mistakes.
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.
Copy link
Collaborator

@danielmeppiel danielmeppiel left a comment

Choose a reason for hiding this comment

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

Thanks for this — the OpenCode primitive mapping is well-researched and the target detection is clean. I'd like to merge this, but the diff is currently unreviewable because ~60-70% is unrelated formatting changes. I found a return-value bug in copy_skill_to_target() that was invisible in the noise, which confirms the concern.

Could you rework this into a clean diff that only touches files/lines related to the OpenCode feature? Here's specifically what I need before merge:

  1. Revert formatting-only changes — rebase on main, only commit your feature lines
  2. Fix copy_skill_to_target() return value — currently deploys to 3 targets (github, claude, opencode) but returns only (github_skill_dir, claude_skill_dir), silently dropping the opencode path. This means orphan files on uninstall.
  3. Verify no double-deploy between the T8 block and integrate_package_skill_opencode() — both appear to copy skills to .opencode/skills/, need to confirm they aren't called together for the same package
  4. Add collision/force tests for at least one OpenCode integrator — current test coverage is ~40% of Claude parity (3 vs 7 for agents, 2 vs 7+ for skills)
  5. Fix the uninstall guard inconsistency — Claude agent re-integration is inside should_integrate() guard, OpenCode is outside

Also worth noting: OpenCode MCP servers are configured via opencode.json (config-level), not file drops like Claude's .mcp.json. Not implementing it here is reasonable since it requires a different integration approach, but I'll file a follow-up issue to track it.

The systemic code duplication (copy-pasting Claude methods and swapping .claude/.opencode/) isn't a blocker for this PR, but adding a 4th target would mean 4×N new methods. I'd like to refactor toward a parameterized target_dir approach in a follow-up.

@danielmeppiel danielmeppiel added the priority/low Accepted but not time-sensitive label Mar 12, 2026
@timvw timvw force-pushed the feat/opencode-target-standalone-v2 branch from 942f4b1 to 29ecaa2 Compare March 13, 2026 13:07
Reworked PR per review feedback from @danielmeppiel:

- Clean feature-only diff (no formatting churn)
- Fix copy_skill_to_target() to return 3-tuple (github, claude, opencode)
- Add T8 block for .opencode/skills/ copy in copy_skill_to_target()
- Wire OpenCode re-integration into uninstall with proper guards
- Add collision/force tests for OpenCode agent and skill integrators
- Verify no double-deploy between copy_skill_to_target and
  integrate_package_skill_opencode (separate codepaths)
- Skill gating correctly uses integrate_vscode/integrate_claude only

Adds opencode as a first-class target across:
- Target detection (auto-detect .opencode/, explicit --target opencode)
- Compile routing (--target opencode generates AGENTS.md)
- Install integration (agents, commands, skills under .opencode/)
- Uninstall cleanup and re-integration
- Managed-file partitioning (agents_opencode, commands_opencode buckets)

1458 tests pass.
@timvw timvw force-pushed the feat/opencode-target-standalone-v2 branch from 29ecaa2 to a995455 Compare March 13, 2026 15:32
@danielmeppiel
Copy link
Collaborator

danielmeppiel commented Mar 14, 2026

Hey @timvw 👋 — thanks for taking this on! I really appreciate you doing the research on OpenCode's primitive mappings and putting together a comprehensive implementation.

Since this PR was opened, I merged #301 which refactored our integration architecture significantly — we now use a data-driven KNOWN_TARGETS + TargetProfile pattern in targets.py that derives integration prefixes, partitioning, and target detection dynamically. This means adding a new target is mostly config (~50 lines) rather than per-integrator method cloning.

Unfortunately that means this PR has deep conflicts — not just merge conflicts, but architectural ones: the hardcoded INTEGRATION_PREFIXES, separate should_integrate_opencode(), copy-paste _opencode() methods, and the fixed-width tuple return from copy_skill_to_target() are all patterns we specifically eliminated.

I'll reimplement OpenCode support on the current architecture and credit you as co-author on all the commits using Co-authored-by. This way you'll show up as a contributor in the repo's contributors list and git shortlog. I'll also credit you in the CHANGELOG and reference this PR.

Let me close this PR once the replacement is merged. Thanks again for the initiative — your target detection logic and test patterns were a useful reference! 🙏

danielmeppiel added a commit that referenced this pull request Mar 14, 2026
Add OpenCode as a new opt-in integration target. When .opencode/
directory exists, apm install deploys:
- Agents → .opencode/agents/<name>.md
- Commands → .opencode/commands/<name>.md (from .prompt.md)
- Skills → .opencode/skills/<name>/SKILL.md
- MCP → opencode.json (new OpenCodeClientAdapter)

OpenCode reads AGENTS.md natively, so apm compile already works.

Implementation:
- Add opencode TargetProfile to KNOWN_TARGETS (targets.py)
- Wire agent, command, skill integrators for .opencode/ (opt-in)
- Create OpenCodeClientAdapter for opencode.json MCP format
- Add opencode to pack/compile target options
- Wire install/uninstall for all OpenCode primitives
- Add 20 unit tests (1778 total passing)
- Update 13 Starlight doc pages
- Credit @timvw in CHANGELOG (PR #257 research)

Closes #257

Co-authored-by: Tim Van Wassenhove <timvw@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request Mar 14, 2026
Add OpenCode as a new opt-in integration target. When .opencode/
directory exists, apm install deploys:
- Agents → .opencode/agents/<name>.md
- Commands → .opencode/commands/<name>.md (from .prompt.md)
- Skills → .opencode/skills/<name>/SKILL.md
- MCP → opencode.json (new OpenCodeClientAdapter)

OpenCode reads AGENTS.md natively, so apm compile already works.

Implementation:
- Add opencode TargetProfile to KNOWN_TARGETS (targets.py)
- Wire agent, command, skill integrators for .opencode/ (opt-in)
- Create OpenCodeClientAdapter for opencode.json MCP format
- Add opencode to pack/compile target options
- Wire install/uninstall for all OpenCode primitives
- Add 20 unit tests (1778 total passing)
- Update 13 Starlight doc pages
- Credit @timvw in CHANGELOG (PR #257 research)

Closes #257

Co-authored-by: Tim Van Wassenhove <timvw@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority/low Accepted but not time-sensitive

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants