Coverage for sources/agentsmgr/population.py: 25%
80 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 02:08 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 02:08 +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''' Command for populating agent content from data sources. '''
24from . import __
25from . import cmdbase as _cmdbase
26from . import core as _core
27from . import exceptions as _exceptions
28from . import generator as _generator
29from . import memorylinks as _memorylinks
30from . import operations as _operations
31from . import renderers as _renderers
32from . import results as _results
33from . import userdata as _userdata
36_scribe = __.provide_scribe( __name__ )
39SourceArgument: __.typx.TypeAlias = __.typx.Annotated[
40 __.tyro.conf.Positional[ str ],
41 __.tyro.conf.arg( help = "Data source (local path or git URL)" ),
42]
43TargetArgument: __.typx.TypeAlias = __.typx.Annotated[
44 __.tyro.conf.Positional[ __.Path ],
45 __.tyro.conf.arg( help = "Target directory for content generation" ),
46]
49def _create_all_symlinks(
50 configuration: __.cabc.Mapping[ str, __.typx.Any ],
51 target: __.Path,
52 mode: str,
53 simulate: bool,
54) -> tuple[ str, ... ]:
55 ''' Creates all symlinks and returns their names for git exclude.
57 Creates memory symlinks for all coders and coder directory
58 symlinks for per-project mode. Returns tuple of all created
59 symlink names.
60 '''
61 all_symlink_names: list[ str ] = [ ]
62 if mode == 'nowhere': return tuple( all_symlink_names )
63 links_attempted, links_created, symlink_names = (
64 _memorylinks.create_memory_symlinks_for_coders(
65 coders = configuration[ 'coders' ],
66 target = target,
67 renderers = _renderers.RENDERERS,
68 simulate = simulate,
69 ) )
70 all_symlink_names.extend( symlink_names )
71 if links_created > 0:
72 _scribe.info(
73 f"Created {links_created}/{links_attempted} memory symlinks" )
74 if mode == 'per-project':
75 ( coder_symlinks_attempted,
76 coder_symlinks_created,
77 coder_symlink_names ) = (
78 _create_coder_directory_symlinks(
79 coders = configuration[ 'coders' ],
80 target = target,
81 renderers = _renderers.RENDERERS,
82 simulate = simulate,
83 ) )
84 all_symlink_names.extend( coder_symlink_names )
85 if coder_symlinks_created > 0:
86 _scribe.info(
87 f"Created {coder_symlinks_created}/"
88 f"{coder_symlinks_attempted} coder directory symlinks" )
89 return tuple( all_symlink_names )
92def _create_coder_directory_symlinks(
93 coders: __.cabc.Sequence[ str ],
94 target: __.Path,
95 renderers: __.cabc.Mapping[ str, __.typx.Any ],
96 simulate: bool = False,
97) -> tuple[ int, int, tuple[ str, ... ] ]:
98 ''' Creates symlinks from .{coder} to .auxiliary/configuration/coders/.
100 For per-project mode, creates symlinks that make coder directories
101 accessible at their expected locations (.claude, .opencode, etc.)
102 while keeping actual files organized under
103 .auxiliary/configuration/coders/.
105 Returns tuple of (attempted, created, symlink_names) where
106 symlink_names contains names of all created symlinks.
107 '''
108 attempted = 0
109 created = 0
110 symlink_names: list[ str ] = [ ]
111 for coder_name in coders:
112 try: renderers[ coder_name ]
113 except KeyError as exception:
114 raise _exceptions.CoderAbsence( coder_name ) from exception
116 # Source: actual location under .auxiliary/configuration/coders/
117 source = (
118 target / '.auxiliary' / 'configuration' / 'coders' / coder_name )
119 # Link: expected location for coder (.claude, .opencode, etc.)
120 link_path = target / f'.{coder_name}'
122 attempted += 1
123 was_created, symlink_name = _memorylinks.create_memory_symlink(
124 source, link_path, simulate )
125 if was_created:
126 created += 1
127 symlink_names.append( symlink_name )
129 # Create .mcp.json symlink for Claude coder specifically
130 if coder_name == 'claude':
131 mcp_source = (
132 target / '.auxiliary' / 'configuration' / 'mcp-servers.json' )
133 mcp_link = target / '.mcp.json'
134 attempted += 1
135 was_created, symlink_name = _memorylinks.create_memory_symlink(
136 mcp_source, mcp_link, simulate )
137 if was_created:
138 created += 1
139 symlink_names.append( symlink_name )
141 return ( attempted, created, tuple( symlink_names ) )
144class PopulateCommand( __.appcore_cli.Command ):
145 ''' Generates dynamic agent content from data sources. '''
147 source: SourceArgument = '.'
148 target: TargetArgument = __.dcls.field( default_factory = __.Path.cwd )
149 profile: __.typx.Annotated[
150 __.typx.Optional[ __.Path ],
151 __.tyro.conf.arg(
152 help = (
153 "Alternative Copier answers file (defaults to "
154 "auto-detected)" ),
155 prefix_name = False ),
156 ] = None
157 simulate: __.typx.Annotated[
158 bool,
159 __.tyro.conf.arg(
160 help = "Dry run mode - show generated content",
161 prefix_name = False ),
162 ] = False
163 mode: __.typx.Annotated[
164 _renderers.TargetMode,
165 __.tyro.conf.arg(
166 help = (
167 "Targeting mode: default (use coder defaults), per-user, "
168 "per-project, or nowhere (skip generation)" ),
169 prefix_name = False ),
170 ] = 'default'
171 update_globals: __.typx.Annotated[
172 bool,
173 __.tyro.conf.arg(
174 help = "Update per-user global files (orthogonal to mode)",
175 prefix_name = False ),
176 ] = False
177 tag_prefix: __.typx.Annotated[
178 __.typx.Optional[ str ],
179 __.tyro.conf.arg(
180 help = (
181 "Prefix for version tags (e.g., 'v', 'stable-', 'prod-'); "
182 "only tags with this prefix are considered and the prefix "
183 "is stripped before version parsing" ),
184 prefix_name = False ),
185 ] = None
187 @_cmdbase.intercept_errors( )
188 async def execute( self, auxdata: __.appcore.state.Globals ) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
189 ''' Generates content from data sources and displays result. '''
190 if not isinstance( auxdata, _core.Globals ): # pragma: no cover
191 raise _exceptions.ContextInvalidity
192 _scribe.info(
193 f"Populating agent content from {self.source} to {self.target}" )
194 configuration = await _cmdbase.retrieve_configuration(
195 self.target, self.profile )
196 coder_count = len( configuration[ 'coders' ] )
197 _scribe.debug( f"Detected configuration with {coder_count} coders" )
198 _scribe.debug( f"Using {self.mode} targeting mode" )
199 prefix = (
200 __.absent if self.tag_prefix is None
201 else self.tag_prefix )
202 location = _cmdbase.retrieve_data_location( self.source, prefix )
203 generator = _generator.ContentGenerator(
204 location = location,
205 configuration = configuration,
206 application_configuration = auxdata.configuration,
207 mode = self.mode,
208 )
209 items_attempted, items_generated = _operations.populate_directory(
210 generator, self.target, self.simulate )
211 _scribe.info( f"Generated {items_generated}/{items_attempted} items" )
212 all_symlink_names = _create_all_symlinks(
213 configuration, self.target, self.mode, self.simulate )
214 if self.update_globals:
215 globals_attempted, globals_updated = (
216 _userdata.populate_globals(
217 location,
218 configuration[ 'coders' ],
219 auxdata.configuration,
220 self.simulate,
221 ) )
222 _scribe.info(
223 f"Updated {globals_updated}/{globals_attempted} "
224 "global files" )
225 if all_symlink_names:
226 excludes_added = _operations.update_git_exclude(
227 self.target, all_symlink_names, self.simulate )
228 if excludes_added > 0:
229 _scribe.info(
230 f"Added {excludes_added} symlink names to "
231 ".git/info/exclude" )
232 result = _results.ContentGenerationResult(
233 source_location = location,
234 target_location = self.target,
235 coders = tuple( configuration[ 'coders' ] ),
236 simulated = self.simulate,
237 items_generated = items_generated,
238 )
239 await _core.render_and_print_result(
240 result, auxdata.display, auxdata.exits )