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

43 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-28 17:44 +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, ExplicitTargetMode, RendererBase 

30 

31 

32class OpencodeRenderer( RendererBase ): 

33 ''' Renderer for OpenCode coder. 

34 

35 Supports both per-user and per-project configuration modes. 

36 Per-user mode respects OPENCODE_CONFIG environment variable 

37 with fallback to configuration overrides and XDG-like default. 

38 ''' 

39 

40 name = 'opencode' 

41 modes_available = frozenset( ( 'per-user', 'per-project' ) ) 

42 mode_default = 'per-project' 

43 memory_filename = 'AGENTS.md' 

44 

45 _LOCATIONS_MAP = __.immut.Dictionary( { 

46 'agents': 'agent', 

47 'commands': 'command', 

48 } ) 

49 

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

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

52  

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

54 than the plural forms used by other coders. 

55 ''' 

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

57 

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

59 ''' Determines template flavor for OpenCode. 

60 

61 OpenCode shares markdown command format with Claude but uses 

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

63 'opencode' for agents. 

64 ''' 

65 if item_type == 'commands': 

66 return 'claude' 

67 return 'opencode' 

68 

69 def provide_project_symlinks( 

70 self, target: __.Path 

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

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

73 

74 OpenCode requires base symlink plus opencode.jsonc symlink 

75 linking to the settings file in the OpenCode configuration. 

76 ''' 

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

78 opencode_source = ( 

79 target / '.auxiliary' / 'configuration' / 'coders' / 

80 'opencode' / 'settings.jsonc' 

81 ) 

82 opencode_link = target / 'opencode.jsonc' 

83 symlinks.append( ( opencode_source, opencode_link ) ) 

84 return symlinks 

85 

86 def resolve_base_directory( 

87 self, 

88 mode: ExplicitTargetMode, 

89 target: __.Path, 

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

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

92 ) -> __.Path: 

93 ''' Resolves base output directory for OpenCode. 

94 

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

96 For per-user mode, respects precedence: OPENCODE_CONFIG 

97 environment variable, configuration file override, or 

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

99 ''' 

100 self.validate_mode( mode ) 

101 if mode == 'per-project': 

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

103 if mode == 'per-user': 

104 return self._resolve_user_directory( configuration, environment ) 

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

106 

107 def _resolve_user_directory( 

108 self, 

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

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

111 ) -> __.Path: 

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

113 

114 Precedence order: 

115 1. OPENCODE_CONFIG environment variable (directory containing 

116 opencode.json settings file) 

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

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

119 ''' 

120 if 'OPENCODE_CONFIG' in environment: 

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

122 return directory.expanduser( ) 

123 coder_configuration = self._extract_coder_configuration( 

124 configuration ) 

125 if 'directory' in coder_configuration: 

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

127 return directory.expanduser( ) 

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

129 

130 def _extract_coder_configuration( 

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

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

133 ''' Extracts configuration for this specific coder. 

134 

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

136 ''' 

137 coders = configuration.get( 'coders', ( ) ) 

138 for coder in coders: 

139 if coder.get( 'name' ) == self.name: 

140 return coder 

141 return { } 

142 

143 

144RENDERERS[ 'opencode' ] = OpencodeRenderer( )