Coverage for sources/agentsmgr/renderers/base.py: 65%

43 statements  

« 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 -*- 

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''' Coder renderer base class and type definitions. 

22 

23 Defines base class for coder-specific renderers which handle 

24 path resolution, targeting mode validation, and output structure 

25 for different AI coding assistants. 

26''' 

27 

28 

29from . import __ 

30 

31 

32TargetMode: __.typx.TypeAlias = __.typx.Literal[ 

33 'default', 'per-user', 'per-project', 'nowhere' ] 

34ExplicitTargetMode: __.typx.TypeAlias = __.typx.Literal[ 

35 'per-user', 'per-project' ] 

36ItemType: __.typx.TypeAlias = __.typx.Literal[ 'commands', 'agents', 'skills' ] 

37 

38 

39class RendererBase( __.immut.Object ): 

40 ''' Base class for coder-specific rendering and path resolution. 

41 

42 Provides interface that all coder renderers must implement for 

43 coder-specific behavior including targeting mode validation and 

44 path resolution for output files. 

45 ''' 

46 

47 name: str 

48 modes_available: frozenset[ ExplicitTargetMode ] 

49 mode_default: ExplicitTargetMode 

50 memory_filename: str 

51 item_types_available: frozenset[ ItemType ] 

52 

53 def validate_mode( self, mode: ExplicitTargetMode ) -> None: 

54 ''' Validates targeting mode is supported by this coder. 

55 

56 Raises TargetModeNoSupport if mode not supported. 

57 ''' 

58 if mode not in self.modes_available: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true

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

60 

61 def resolve_base_directory( 

62 self, 

63 mode: ExplicitTargetMode, 

64 target: __.Path, 

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

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

67 ) -> __.Path: 

68 ''' Resolves base output directory for this coder. 

69 

70 Determines appropriate output location based on targeting mode, 

71 respecting precedence of environment variables over file 

72 configuration over coder defaults. For per-user mode, checks 

73 environment first, then configuration file overrides, then 

74 falls back to coder-specific defaults. For per-project mode, 

75 constructs path within project structure. 

76 ''' 

77 raise NotImplementedError 

78 

79 def produce_output_structure( 

80 self, item_type: str, category: __.Absential[ str ] = __.absent 

81 ) -> str: 

82 ''' Produces subdirectory structure for item type. 

83 

84 Translates generic item type to coder-specific directory 

85 structure with optional category-based organization. Most 

86 coders use same structure, but some may have different 

87 conventions. Category enables hierarchical organization 

88 (e.g., commands/deploy/kubernetes for nested structure). 

89 ''' 

90 dirname = self.calculate_directory_location( item_type ) 

91 if __.is_absent( category ): return dirname 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was always true

92 return f"{dirname}/{category}" 

93 

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

95 ''' Returns directory name for item type. Override in subclasses. ''' 

96 return item_type 

97 

98 def calculate_artifact_pattern( self, item_type: str ) -> str: 

99 ''' Returns file glob pattern for artifacts of this item type. 

100 

101 Mirrors the per-renderer ownership of artifact shape that 

102 ``calculate_directory_location`` provides for directory names: 

103 the renderer is the authoritative source for both the 

104 location and the file pattern of the artifacts in that 

105 location. Default is Markdown (``*.md``); subclasses may 

106 override for coders whose artifacts use a different file 

107 extension. The contract returns a single glob expression 

108 suitable for one ``Path.glob`` call. 

109 ''' 

110 return '*.md' 

111 

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

113 ''' Determines template flavor (pioneering coder name) for item type. 

114 

115 Returns the name of the pioneering coder whose template format 

116 should be used for this item type. For example, Claude pioneered 

117 the markdown command format. 

118 Each coder specifies which flavor it uses for each item type. 

119 ''' 

120 return 'claude' 

121 

122 def provide_project_symlinks( 

123 self, target: __.Path 

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

125 ''' Provides symlinks required for coder in per-project mode. 

126 

127 Returns sequence of (source, link_path) tuples where source 

128 is the target path and link_path is where the symlink should 

129 be created. Default implementation returns base symlink from 

130 .auxiliary/configuration/coders/{coder_name} to .{coder_name}. 

131 

132 Subclasses may override to provide additional coder-specific 

133 symlinks (e.g., Claude's .mcp.json, OpenCode's opencode.jsonc). 

134 ''' 

135 source = ( 

136 target / '.auxiliary' / 'configuration' / 'coders' / self.name ) 

137 link_path = target / f'.{self.name}' 

138 return [ ( source, link_path ) ] 

139 

140 

141RENDERERS: __.accret.Dictionary[ str, RendererBase ] = ( 

142 __.accret.Dictionary( ) ) 

143 

144 

145def extract_coder_configuration( 

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

147 coder_name: str, 

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

149 ''' Extracts coder-specific configuration from mixed coder formats. 

150 

151 Supports both list-of-name and list-of-mapping coders formats: 

152 - coders = [ 'codex', 'claude' ] 

153 - coders = [ { 'name': 'codex', ... }, ... ] 

154 ''' 

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

156 for coder in coders: 156 ↛ 166line 156 didn't jump to line 166 because the loop on line 156 didn't complete

157 if isinstance( coder, str ): 157 ↛ 161line 157 didn't jump to line 161 because the condition on line 157 was always true

158 if coder == coder_name: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true

159 return { } 

160 continue 

161 if not isinstance( coder, dict ): 

162 continue 

163 candidate = __.typx.cast( dict[ str, __.typx.Any ], coder ) 

164 if candidate.get( 'name' ) == coder_name: 

165 return candidate 

166 return { }