Coverage for sources / agentsmgr / renderers / opencode.py: 66%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 23:00 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

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#============================================================================# 

19 

20 

21''' OpenCode renderer implementation. 

22 

23 Provides path resolution and targeting mode validation for OpenCode, 

24 which supports both per-user and per-project configuration. 

25''' 

26 

27 

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 

33 

34 

35class OpencodeRenderer( _RendererBase ): 

36 ''' Renderer for OpenCode coder. 

37 

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 ''' 

42 

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' ) ) 

48 

49 _LOCATIONS_MAP = __.immut.Dictionary( { 

50 'agents': 'agent', 

51 'commands': 'command', 

52 } ) 

53 

54 def calculate_directory_location( self, item_type: str ) -> str: 

55 ''' Returns singular directory names for OpenCode configuration. 

56  

57 OpenCode expects singular directory names (agent, command) rather 

58 than the plural forms used by other coders. 

59 ''' 

60 return self._LOCATIONS_MAP.get( item_type, item_type ) 

61 

62 def get_template_flavor( self, item_type: str ) -> str: 

63 ''' Determines template flavor for OpenCode. 

64 

65 OpenCode shares markdown command format with Claude but uses 

66 its own agent format, so returns 'claude' for commands and 

67 'opencode' for agents. 

68 ''' 

69 if item_type == 'commands': 

70 return 'claude' 

71 return 'opencode' 

72 

73 def provide_project_symlinks( 

74 self, target: __.Path 

75 ) -> __.cabc.Sequence[ tuple[ __.Path, __.Path ] ]: 

76 ''' Provides symlinks required for OpenCode in per-project mode. 

77 

78 OpenCode requires base symlink plus opencode.jsonc symlink 

79 linking to the settings file in the OpenCode configuration. 

80 ''' 

81 symlinks = list( super( ).provide_project_symlinks( target ) ) 

82 opencode_source = ( 

83 target / '.auxiliary' / 'configuration' / 'coders' / 

84 'opencode' / 'settings.jsonc' 

85 ) 

86 opencode_link = target / 'opencode.jsonc' 

87 symlinks.append( ( opencode_source, opencode_link ) ) 

88 return symlinks 

89 

90 def resolve_base_directory( 

91 self, 

92 mode: _ExplicitTargetMode, 

93 target: __.Path, 

94 configuration: __.cabc.Mapping[ str, __.typx.Any ], 

95 environment: __.cabc.Mapping[ str, str ], 

96 ) -> __.Path: 

97 ''' Resolves base output directory for OpenCode. 

98 

99 For per-project mode, returns .opencode in project root. 

100 For per-user mode, respects precedence: OPENCODE_CONFIG 

101 environment variable, configuration file override, or 

102 XDG-like ~/.config/opencode default. 

103 ''' 

104 self.validate_mode( mode ) 

105 if mode == 'per-project': 

106 return target / ".auxiliary/configuration/coders/opencode" 

107 if mode == 'per-user': 107 ↛ 109line 107 didn't jump to line 109 because the condition on line 107 was always true

108 return self._resolve_user_directory( configuration, environment ) 

109 raise __.TargetModeNoSupport( self.name, mode ) 

110 

111 def _resolve_user_directory( 

112 self, 

113 configuration: __.cabc.Mapping[ str, __.typx.Any ], 

114 environment: __.cabc.Mapping[ str, str ], 

115 ) -> __.Path: 

116 ''' Resolves per-user directory following precedence rules. 

117 

118 Precedence order: 

119 1. OPENCODE_CONFIG environment variable (directory containing 

120 opencode.json settings file) 

121 2. Configuration file override (directory for this coder) 

122 3. XDG-like default ~/.config/opencode 

123 ''' 

124 if 'OPENCODE_CONFIG' in environment: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 directory = __.Path( environment[ 'OPENCODE_CONFIG' ] ) 

126 return directory.expanduser( ) 

127 coder_configuration = self._extract_coder_configuration( 

128 configuration ) 

129 if 'directory' in coder_configuration: 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 directory = __.Path( coder_configuration[ 'directory' ] ) 

131 return directory.expanduser( ) 

132 return __.Path.home( ) / '.config' / 'opencode' 

133 

134 def _extract_coder_configuration( 

135 self, configuration: __.cabc.Mapping[ str, __.typx.Any ] 

136 ) -> __.cabc.Mapping[ str, __.typx.Any ]: 

137 ''' Extracts configuration for this specific coder. 

138 

139 Looks for coder entry in configuration coders array by name. 

140 ''' 

141 return _extract_coder_configuration( configuration, self.name ) 

142 

143 

144_RENDERERS[ 'opencode' ] = OpencodeRenderer( )