Coverage for sources/agentsmgr/renderers/opencode.py: 84%
40 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 21:08 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 21:08 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21''' OpenCode renderer implementation.
23 Provides path resolution and targeting mode validation for OpenCode,
24 which supports both per-user and per-project configuration.
25'''
28from . import __
29from .base import RENDERERS as _RENDERERS
30from .base import ExplicitTargetMode as _ExplicitTargetMode
31from .base import RendererBase as _RendererBase
32from .base import extract_coder_configuration as _extract_coder_configuration
35class OpencodeRenderer( _RendererBase ):
36 ''' Renderer for OpenCode coder.
38 Supports both per-user and per-project configuration modes.
39 Per-user mode respects OPENCODE_CONFIG environment variable
40 with fallback to configuration overrides and XDG-like default.
41 '''
43 name = 'opencode'
44 modes_available = frozenset( ( 'per-user', 'per-project' ) )
45 mode_default = 'per-project'
46 memory_filename = 'AGENTS.md'
47 item_types_available = frozenset( ( 'commands', 'agents', 'skills' ) )
49 def get_template_flavor( self, item_type: str ) -> str:
50 ''' Determines template flavor for OpenCode.
52 OpenCode shares markdown command format with Claude but uses
53 its own agent format, so returns 'claude' for commands and
54 'opencode' for agents.
55 '''
56 if item_type == 'commands':
57 return 'claude'
58 return 'opencode'
60 def provide_project_symlinks(
61 self, target: __.Path
62 ) -> __.cabc.Sequence[ tuple[ __.Path, __.Path ] ]:
63 ''' Provides symlinks required for OpenCode in per-project mode.
65 OpenCode requires base symlink plus opencode.jsonc symlink
66 linking to the settings file in the OpenCode configuration.
67 '''
68 symlinks = list( super( ).provide_project_symlinks( target ) )
69 opencode_source = (
70 target / '.auxiliary' / 'configuration' / 'coders' /
71 'opencode' / 'settings.jsonc'
72 )
73 opencode_link = target / 'opencode.jsonc'
74 symlinks.append( ( opencode_source, opencode_link ) )
75 return symlinks
77 def resolve_base_directory(
78 self,
79 mode: _ExplicitTargetMode,
80 target: __.Path,
81 configuration: __.cabc.Mapping[ str, __.typx.Any ],
82 environment: __.cabc.Mapping[ str, str ],
83 ) -> __.Path:
84 ''' Resolves base output directory for OpenCode.
86 For per-project mode, returns .opencode in project root.
87 For per-user mode, respects precedence: OPENCODE_CONFIG
88 environment variable, configuration file override, or
89 XDG-like ~/.config/opencode default.
90 '''
91 self.validate_mode( mode )
92 if mode == 'per-project':
93 return target / ".auxiliary/configuration/coders/opencode"
94 if mode == 'per-user': 94 ↛ 96line 94 didn't jump to line 96 because the condition on line 94 was always true
95 return self._resolve_user_directory( configuration, environment )
96 raise __.TargetModeNoSupport( self.name, mode )
98 def _resolve_user_directory(
99 self,
100 configuration: __.cabc.Mapping[ str, __.typx.Any ],
101 environment: __.cabc.Mapping[ str, str ],
102 ) -> __.Path:
103 ''' Resolves per-user directory following precedence rules.
105 Precedence order:
106 1. OPENCODE_CONFIG environment variable (directory containing
107 opencode.json settings file)
108 2. Configuration file override (directory for this coder)
109 3. XDG-like default ~/.config/opencode
110 '''
111 if 'OPENCODE_CONFIG' in environment: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 directory = __.Path( environment[ 'OPENCODE_CONFIG' ] )
113 return directory.expanduser( )
114 coder_configuration = self._extract_coder_configuration(
115 configuration )
116 if 'directory' in coder_configuration: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 directory = __.Path( coder_configuration[ 'directory' ] )
118 return directory.expanduser( )
119 return __.Path.home( ) / '.config' / 'opencode'
121 def _extract_coder_configuration(
122 self, configuration: __.cabc.Mapping[ str, __.typx.Any ]
123 ) -> __.cabc.Mapping[ str, __.typx.Any ]:
124 ''' Extracts configuration for this specific coder.
126 Looks for coder entry in configuration coders array by name.
127 '''
128 return _extract_coder_configuration( configuration, self.name )
131_RENDERERS[ 'opencode' ] = OpencodeRenderer( )