feat: add standalone OpenCode target integration#257
feat: add standalone OpenCode target integration#257timvw wants to merge 1 commit intomicrosoft:mainfrom
Conversation
26b6291 to
cf34c59
Compare
There was a problem hiding this comment.
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 opencodegenerates 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
opencodetarget 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_opencodeis included in the condition that callsintegrate_package_skill(), which deploys skills to.github/skills/(and.claude/skills/when present). If--target opencodeis meant to be a standalone target, this will still write VSCode/Claude skill artifacts even when VSCode/Claude integration is disabled. Consider gatingintegrate_package_skill()onintegrate_vscode or integrate_claudeonly, and usingintegrate_package_skill_opencode()for the OpenCode-only path (or making the skill integrator target-aware).
if (
integrate_vscode
or integrate_claude
or integrate_opencode
):
src/apm_cli/commands/install.py
Outdated
| 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/" | ||
| ) |
There was a problem hiding this comment.
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.
| 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/" | |
| ) |
src/apm_cli/commands/install.py
Outdated
| if ( | ||
| integrate_vscode | ||
| or integrate_claude | ||
| or integrate_opencode | ||
| ): |
There was a problem hiding this comment.
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.
| # 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: |
There was a problem hiding this comment.
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.
| 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)", |
There was a problem hiding this comment.
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.
| agent_integrator.integrate_package_agents_opencode( | ||
| pkg_info, project_root | ||
| ) | ||
| skill_integrator.integrate_package_skill(pkg_info, project_root) |
There was a problem hiding this comment.
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.
| skill_integrator.integrate_package_skill(pkg_info, project_root) | |
| if integrate_vscode or integrate_claude: | |
| skill_integrator.integrate_package_skill( | |
| pkg_info, project_root | |
| ) |
There was a problem hiding this comment.
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:
- Revert formatting-only changes — rebase on main, only commit your feature lines
- 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. - 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 - 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)
- 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.
942f4b1 to
29ecaa2
Compare
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.
29ecaa2 to
a995455
Compare
|
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 Unfortunately that means this PR has deep conflicts — not just merge conflicts, but architectural ones: the hardcoded I'll reimplement OpenCode support on the current architecture and credit you as co-author on all the commits using 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! 🙏 |
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>
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>
Summary
opencodeas a standalone target in target detection and compile routing (including--target opencode).opencode/agents,.opencode/commands, and.opencode/skillscommands/install.py,commands/uninstall.py)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 -quv run --extra dev pytest tests/unit -quv 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 -qNotes
cli.pyinto command modules; OpenCode install/uninstall integration was ported to those modulespytest -q/integration runs were interrupted by the tool session before completion, so unit and targeted regression coverage is provided above