11"""Target detection for auto-selecting compilation and integration targets.
22
33This module implements the auto-detection pattern for determining which agent
4- targets (VSCode/Copilot vs Claude) should be used based on existing project
5- structure and configuration.
4+ targets (VSCode/Copilot, Claude, OpenCode ) should be used based on existing
5+ project structure and configuration.
66
77Detection priority (highest to lowest):
881. Explicit --target flag (always wins)
992. apm.yml target setting (top-level field)
10103. Auto-detect from existing folders:
11- - .github/ exists AND .claude/ doesn't → vscode
12- - .claude/ exists AND .github/ doesn't → claude
13- - Both exist → all
14- - Neither exists → minimal (AGENTS.md only, no folder integration)
11+ - .github/ exists only → vscode
12+ - .claude/ exists only → claude
13+ - .opencode/ exists only → opencode
14+ - Multiple integration folders exist → all
15+ - None exist → minimal (AGENTS.md only, no folder integration)
1516"""
1617
1718from pathlib import Path
1819from typing import Literal , Optional , Tuple
1920
2021# Valid target values
21- TargetType = Literal ["vscode" , "claude" , "all" , "minimal" ]
22+ TargetType = Literal ["vscode" , "claude" , "opencode" , " all" , "minimal" ]
2223
2324
2425def detect_target (
@@ -27,12 +28,12 @@ def detect_target(
2728 config_target : Optional [str ] = None ,
2829) -> Tuple [TargetType , str ]:
2930 """Detect the appropriate target for compilation and integration.
30-
31+
3132 Args:
3233 project_root: Root directory of the project
3334 explicit_target: Explicitly provided --target flag value
3435 config_target: Target from apm.yml top-level 'target' field
35-
36+
3637 Returns:
3738 Tuple of (target, reason) where:
3839 - target: The detected target type
@@ -44,39 +45,60 @@ def detect_target(
4445 return "vscode" , "explicit --target flag"
4546 elif explicit_target == "claude" :
4647 return "claude" , "explicit --target flag"
48+ elif explicit_target == "opencode" :
49+ return "opencode" , "explicit --target flag"
4750 elif explicit_target == "all" :
4851 return "all" , "explicit --target flag"
49-
52+
5053 # Priority 2: apm.yml target setting
5154 if config_target :
5255 if config_target in ("vscode" , "agents" ):
5356 return "vscode" , "apm.yml target"
5457 elif config_target == "claude" :
5558 return "claude" , "apm.yml target"
59+ elif config_target == "opencode" :
60+ return "opencode" , "apm.yml target"
5661 elif config_target == "all" :
5762 return "all" , "apm.yml target"
58-
63+
5964 # Priority 3: Auto-detect from existing folders
6065 github_exists = (project_root / ".github" ).exists ()
6166 claude_exists = (project_root / ".claude" ).exists ()
62-
63- if github_exists and not claude_exists :
67+ opencode_exists = (project_root / ".opencode" ).exists ()
68+
69+ enabled_targets = []
70+ if github_exists :
71+ enabled_targets .append ("vscode" )
72+ if claude_exists :
73+ enabled_targets .append ("claude" )
74+ if opencode_exists :
75+ enabled_targets .append ("opencode" )
76+
77+ if enabled_targets == ["vscode" ]:
6478 return "vscode" , "detected .github/ folder"
65- elif claude_exists and not github_exists :
79+ elif enabled_targets == [ "claude" ] :
6680 return "claude" , "detected .claude/ folder"
67- elif github_exists and claude_exists :
68- return "all" , "detected both .github/ and .claude/ folders"
81+ elif enabled_targets == ["opencode" ]:
82+ return "opencode" , "detected .opencode/ folder"
83+ elif len (enabled_targets ) > 1 :
84+ labels = {
85+ "vscode" : ".github/" ,
86+ "claude" : ".claude/" ,
87+ "opencode" : ".opencode/" ,
88+ }
89+ joined = ", " .join (labels [target ] for target in enabled_targets )
90+ return "all" , f"detected multiple integration folders ({ joined } )"
6991 else :
7092 # Neither folder exists - minimal output
71- return "minimal" , "no .github/ or .claude / folder found"
93+ return "minimal" , "no .github/, .claude/, or .opencode / folder found"
7294
7395
7496def should_integrate_vscode (target : TargetType ) -> bool :
7597 """Check if VSCode integration should be performed.
76-
98+
7799 Args:
78100 target: The detected or configured target
79-
101+
80102 Returns:
81103 bool: True if VSCode integration (prompts, agents) should run
82104 """
@@ -85,37 +107,49 @@ def should_integrate_vscode(target: TargetType) -> bool:
85107
86108def should_integrate_claude (target : TargetType ) -> bool :
87109 """Check if Claude integration should be performed.
88-
110+
89111 Args:
90112 target: The detected or configured target
91-
113+
92114 Returns:
93115 bool: True if Claude integration (commands, skills) should run
94116 """
95117 return target in ("claude" , "all" )
96118
97119
120+ def should_integrate_opencode (target : TargetType ) -> bool :
121+ """Check if OpenCode integration should be performed.
122+
123+ Args:
124+ target: The detected or configured target
125+
126+ Returns:
127+ bool: True if OpenCode integration should run
128+ """
129+ return target in ("opencode" , "all" )
130+
131+
98132def should_compile_agents_md (target : TargetType ) -> bool :
99133 """Check if AGENTS.md should be compiled.
100-
101- AGENTS.md is generated for vscode, all, and minimal targets.
134+
135+ AGENTS.md is generated for vscode, opencode, all, and minimal targets.
102136 It's the universal format that works everywhere.
103-
137+
104138 Args:
105139 target: The detected or configured target
106-
140+
107141 Returns:
108142 bool: True if AGENTS.md should be generated
109143 """
110- return target in ("vscode" , "all" , "minimal" )
144+ return target in ("vscode" , "opencode" , " all" , "minimal" )
111145
112146
113147def should_compile_claude_md (target : TargetType ) -> bool :
114148 """Check if CLAUDE.md should be compiled.
115-
149+
116150 Args:
117151 target: The detected or configured target
118-
152+
119153 Returns:
120154 bool: True if CLAUDE.md should be generated
121155 """
@@ -124,17 +158,18 @@ def should_compile_claude_md(target: TargetType) -> bool:
124158
125159def get_target_description (target : TargetType ) -> str :
126160 """Get a human-readable description of what will be generated for a target.
127-
161+
128162 Args:
129163 target: The target type
130-
164+
131165 Returns:
132166 str: Description of output files
133167 """
134168 descriptions = {
135169 "vscode" : "AGENTS.md + .github/prompts/ + .github/agents/" ,
136170 "claude" : "CLAUDE.md + .claude/commands/ + .claude/agents/ + .claude/skills/" ,
137- "all" : "AGENTS.md + CLAUDE.md + .github/ + .claude/" ,
171+ "opencode" : "AGENTS.md + .opencode/commands/ + .opencode/skills/" ,
172+ "all" : "AGENTS.md + CLAUDE.md + .github/ + .claude/ + .opencode/" ,
138173 "minimal" : "AGENTS.md only (create .github/ or .claude/ for full integration)" ,
139174 }
140175 return descriptions .get (target , "unknown target" )
0 commit comments