Coverage for sources / agentsmgr / renderers / base.py: 64%
41 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 23:00 +0000
« 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 -*-
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''' Coder renderer base class and type definitions.
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'''
29from . import __
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' ]
39class RendererBase( __.immut.Object ):
40 ''' Base class for coder-specific rendering and path resolution.
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 '''
47 name: str
48 modes_available: frozenset[ ExplicitTargetMode ]
49 mode_default: ExplicitTargetMode
50 memory_filename: str
51 item_types_available: frozenset[ ItemType ]
53 def validate_mode( self, mode: ExplicitTargetMode ) -> None:
54 ''' Validates targeting mode is supported by this coder.
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 )
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.
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
79 def produce_output_structure(
80 self, item_type: str, category: __.Absential[ str ] = __.absent
81 ) -> str:
82 ''' Produces subdirectory structure for item type.
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}"
94 def calculate_directory_location( self, item_type: str ) -> str:
95 ''' Returns directory name for item type. Override in subclasses. '''
96 return item_type
98 def get_template_flavor( self, item_type: str ) -> str:
99 ''' Determines template flavor (pioneering coder name) for item type.
101 Returns the name of the pioneering coder whose template format
102 should be used for this item type. For example, Claude pioneered
103 the markdown command format, Gemini pioneered the TOML format.
104 Each coder specifies which flavor it uses for each item type.
105 '''
106 return 'claude'
108 def provide_project_symlinks(
109 self, target: __.Path
110 ) -> __.cabc.Sequence[ tuple[ __.Path, __.Path ] ]:
111 ''' Provides symlinks required for coder in per-project mode.
113 Returns sequence of (source, link_path) tuples where source
114 is the target path and link_path is where the symlink should
115 be created. Default implementation returns base symlink from
116 .auxiliary/configuration/coders/{coder_name} to .{coder_name}.
118 Subclasses may override to provide additional coder-specific
119 symlinks (e.g., Claude's .mcp.json, OpenCode's opencode.jsonc).
120 '''
121 source = (
122 target / '.auxiliary' / 'configuration' / 'coders' / self.name )
123 link_path = target / f'.{self.name}'
124 return [ ( source, link_path ) ]
127RENDERERS: __.accret.Dictionary[ str, RendererBase ] = (
128 __.accret.Dictionary( ) )
131def extract_coder_configuration(
132 configuration: __.cabc.Mapping[ str, __.typx.Any ],
133 coder_name: str,
134) -> __.cabc.Mapping[ str, __.typx.Any ]:
135 ''' Extracts coder-specific configuration from mixed coder formats.
137 Supports both list-of-name and list-of-mapping coders formats:
138 - coders = [ 'codex', 'claude' ]
139 - coders = [ { 'name': 'codex', ... }, ... ]
140 '''
141 coders = configuration.get( 'coders', ( ) )
142 for coder in coders: 142 ↛ 152line 142 didn't jump to line 152 because the loop on line 142 didn't complete
143 if isinstance( coder, str ): 143 ↛ 147line 143 didn't jump to line 147 because the condition on line 143 was always true
144 if coder == coder_name: 144 ↛ 146line 144 didn't jump to line 146 because the condition on line 144 was always true
145 return { }
146 continue
147 if not isinstance( coder, dict ):
148 continue
149 candidate = __.typx.cast( dict[ str, __.typx.Any ], coder )
150 if candidate.get( 'name' ) == coder_name:
151 return candidate
152 return { }