Coverage for sources / agentsmgr / memorylinks.py: 8%

46 statements  

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

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''' Memory file symlink management for coder configurations. 

22 

23 Provides functionality to create symlinks from coder-specific memory 

24 filenames to shared project conventions file. Follows patterns from 

25 .auxiliary/scripts/prepare-agents for consistent behavior. 

26''' 

27 

28 

29from . import __ 

30from . import exceptions as _exceptions 

31 

32 

33_scribe = __.provide_scribe( __name__ ) 

34 

35 

36def create_memory_symlink( 

37 source: __.Path, 

38 link_path: __.Path, 

39 simulate: bool = False, 

40) -> tuple[ bool, str ]: 

41 ''' Creates symlink from coder memory file to project conventions. 

42 

43 Follows patterns from .auxiliary/scripts/prepare-agents: 

44 - If link is symlink to correct target: Skip silently 

45 - If link is symlink to wrong target: Update it 

46 - If link is broken symlink: Remove and recreate 

47 - If link is regular file/directory: Warn and skip 

48 - If link doesn't exist: Create symlink 

49 

50 Returns tuple of (created, symlink_name) where created indicates 

51 if symlink was created/updated and symlink_name is the name 

52 relative to parent directory. 

53 ''' 

54 symlink_name = link_path.name 

55 try: 

56 relative_source = __.os.path.relpath( 

57 source, start = link_path.parent ) 

58 except ValueError: 

59 relative_source = str( source.resolve( ) ) 

60 if link_path.is_symlink( ): 

61 try: current_target = __.os.readlink( link_path ) 

62 except OSError as exception: 

63 _scribe.warning( 

64 f"Cannot read symlink {link_path}: {exception}." ) 

65 return ( False, symlink_name ) 

66 if current_target == relative_source: 

67 return ( False, symlink_name ) 

68 _scribe.info( 

69 f"Updating symlink {link_path.name}: " 

70 f"{current_target} → {relative_source}" ) 

71 if not simulate: link_path.unlink( ) 

72 elif link_path.exists( ): 

73 _scribe.warning( 

74 f"File or directory already exists at {link_path}. Skipping." ) 

75 return ( False, symlink_name ) 

76 elif not link_path.exists( ) and link_path.is_symlink( ): 

77 _scribe.info( f"Fixing broken symlink: {link_path.name}" ) 

78 if not simulate: link_path.unlink( ) 

79 if not simulate: 

80 link_path.symlink_to( relative_source ) 

81 _scribe.info( f"Created memory symlink: {link_path.name}" ) 

82 else: 

83 _scribe.info( 

84 f"[SIMULATE] Would create symlink: " 

85 f"{link_path.name} → {relative_source}" ) 

86 return ( True, symlink_name ) 

87 

88 

89def create_memory_symlinks_for_coders( 

90 coders: __.cabc.Sequence[ str ], 

91 target: __.Path, 

92 renderers: __.cabc.Mapping[ str, __.typx.Any ], 

93 simulate: bool = False, 

94) -> tuple[ int, int, tuple[ str, ... ] ]: 

95 ''' Creates memory symlinks for all configured coders. 

96 

97 Memory symlinks are always created at project root, pointing to 

98 project-specific conventions file. They are created regardless 

99 of targeting mode since memory files are project-specific. 

100 

101 Returns tuple of (attempted, created, symlink_names) where 

102 symlink_names contains names of all symlinks (both newly created 

103 and pre-existing). 

104 ''' 

105 source = target / '.auxiliary' / 'configuration' / 'AGENTS.md' 

106 if not source.exists( ): 

107 raise _exceptions.MemoryFileAbsence( source ) 

108 attempted = 0 

109 created = 0 

110 symlink_names: list[ str ] = [ ] 

111 for coder_name in coders: 

112 try: renderer = renderers[ coder_name ] 

113 except KeyError as exception: 

114 raise _exceptions.CoderAbsence( coder_name ) from exception 

115 link_path = target / renderer.memory_filename 

116 attempted += 1 

117 was_created, symlink_name = create_memory_symlink( 

118 source, link_path, simulate ) 

119 if was_created: created += 1 

120 symlink_names.append( symlink_name ) 

121 return ( attempted, created, tuple( symlink_names ) )