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

46 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 21:55 +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 

32 

33 

34class OpencodeRenderer( _RendererBase ): 

35 ''' Renderer for OpenCode coder. 

36 

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

38 Per-user mode respects OPENCODE_CONFIG environment variable 

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

40 ''' 

41 

42 name = 'opencode' 

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

44 mode_default = 'per-project' 

45 memory_filename = 'AGENTS.md' 

46 item_types_available = frozenset( ( 'commands', 'agents', 'skills' ) ) 

47 

48 _LOCATIONS_MAP = __.immut.Dictionary( { 

49 'agents': 'agent', 

50 'commands': 'command', 

51 } ) 

52 

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

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

55  

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

57 than the plural forms used by other coders. 

58 ''' 

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

60 

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

62 ''' Determines template flavor for OpenCode. 

63 

64 OpenCode shares markdown command format with Claude but uses 

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

66 'opencode' for agents. 

67 ''' 

68 if item_type == 'commands': 

69 return 'claude' 

70 return 'opencode' 

71 

72 def provide_project_symlinks( 

73 self, target: __.Path 

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

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

76 

77 OpenCode requires base symlink plus opencode.jsonc symlink 

78 linking to the settings file in the OpenCode configuration. 

79 ''' 

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

81 opencode_source = ( 

82 target / '.auxiliary' / 'configuration' / 'coders' / 

83 'opencode' / 'settings.jsonc' 

84 ) 

85 opencode_link = target / 'opencode.jsonc' 

86 symlinks.append( ( opencode_source, opencode_link ) ) 

87 return symlinks 

88 

89 def resolve_base_directory( 

90 self, 

91 mode: _ExplicitTargetMode, 

92 target: __.Path, 

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

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

95 ) -> __.Path: 

96 ''' Resolves base output directory for OpenCode. 

97 

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

99 For per-user mode, respects precedence: OPENCODE_CONFIG 

100 environment variable, configuration file override, or 

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

102 ''' 

103 self.validate_mode( mode ) 

104 if mode == 'per-project': 104 ↛ 106line 104 didn't jump to line 106 because the condition on line 104 was always true

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

106 if mode == 'per-user': 

107 return self._resolve_user_directory( configuration, environment ) 

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

109 

110 def _resolve_user_directory( 

111 self, 

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

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

114 ) -> __.Path: 

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

116 

117 Precedence order: 

118 1. OPENCODE_CONFIG environment variable (directory containing 

119 opencode.json settings file) 

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

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

122 ''' 

123 if 'OPENCODE_CONFIG' in environment: 

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

125 return directory.expanduser( ) 

126 coder_configuration = self._extract_coder_configuration( 

127 configuration ) 

128 if 'directory' in coder_configuration: 

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

130 return directory.expanduser( ) 

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

132 

133 def _extract_coder_configuration( 

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

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

136 ''' Extracts configuration for this specific coder. 

137 

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

139 ''' 

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

141 for coder in coders: 

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

143 return coder 

144 return { } 

145 

146 

147_RENDERERS[ 'opencode' ] = OpencodeRenderer( )