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