Coverage for src / library_skills / installer.py: 100%

144 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-03 20:19 +0000

1import json 1gsthuviwx

2import os 1gsthuviwx

3import shutil 1gsthuviwx

4from dataclasses import dataclass 1gsthuviwx

5from importlib import metadata, resources 1gsthuviwx

6from pathlib import Path 1gsthuviwx

7 

8from .scanner import Skill 1gsthuviwx

9 

10UNIVERSAL_SKILLS_DIR = ".agents/skills" 1gsthuviwx

11CLAUDE_SKILLS_DIR = ".claude/skills" 1gsthuviwx

12TOOL_SKILL_NAME = "library-skills" 1gsthuviwx

13TOOL_SKILL_MARKER = ".library-skills.json" 1gsthuviwx

14TOOL_SKILL_KIND = "tool-skill" 1gsthuviwx

15 

16 

17@dataclass(frozen=True) 1gsthuviwx

18class InstallTarget: 1gsthuviwx

19 """A project-level skill installation target.""" 

20 

21 name: str 1ghi

22 path: Path 1ghi

23 

24 

25@dataclass(frozen=True) 1gsthuviwx

26class InstalledSkill: 1gsthuviwx

27 """A skill directory found in an installation target.""" 

28 

29 name: str 1ghi

30 type: str 1ghi

31 path: Path 1ghi

32 target: Path | None = None 1gsthuviwx

33 has_skill_md: bool = False 1gsthuviwx

34 

35 

36@dataclass(frozen=True) 1gsthuviwx

37class ToolSkillStatus: 1gsthuviwx

38 """Status for the copied Library Skills tool skill in a target.""" 

39 

40 target: InstallTarget 1ghi

41 path: Path 1ghi

42 status: str 1ghi

43 

44 

45def get_target_dirs( 1gsthuviwx

46 project_root: Path, *, include_claude: bool = False 

47) -> list[InstallTarget]: 

48 """Get project-level target directories. 

49 

50 The universal .agents/skills target is always included. Claude-compatible 

51 installs are additional and never replace the universal target. 

52 """ 

53 targets = [ 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb

54 InstallTarget(name="universal", path=project_root / UNIVERSAL_SKILLS_DIR) 

55 ] 

56 if include_claude: 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb

57 targets.append( 29b!b] - #by p d j T : m 6bW X Y Rb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 Sbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 Tbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb

58 InstallTarget( 

59 name="claude-compatible", path=project_root / CLAUDE_SKILLS_DIR 

60 ) 

61 ) 

62 return targets 29b!b] - #by p d j T : m 6bW X Y LbRb~b^ _ ac` bc; 8 9 ! { ccDca tb5 K B ub^bGcvbwb$b%b| . 'bz q e k U = n 7bZ 0 1 MbSbdc} ~ ecabfc? # $ % bbgcEcb xb6 L C yb_bHczbAb(b)bcb/ *bA r f l V @ o 8b2 3 4 NbTbhcdbebicfbjc[ ' ( ) gbkcFcc Bb7 M D Cb`bIcDbEb

63 

64 

65def get_all_target_dirs(project_root: Path) -> list[InstallTarget]: 1gsthuviwx

66 """Get all supported project-level target directories.""" 

67 return get_target_dirs(project_root, include_claude=True) 29b!b] - #by p d j T : m W X Y Rb~b^ _ ac` bc; 8 9 ! { ccDca 5 K B ub^bFbGb$b%b| . 'bz q e k U = n Z 0 1 Sbdc} ~ ecabfc? # $ % bbgcEcb 6 L C yb_bHbIb(b)bcb/ *bA r f l V @ o 2 3 4 Tbhcdbebicfbjc[ ' ( ) gbkcFcc 7 M D Cb`bJbKb

68 

69 

70def get_default_install_target_dirs(project_root: Path) -> list[InstallTarget]: 1gsthuviwx

71 """Get interactive install target defaults from project state. 

72 

73 Parent directories are enough to signal that the project uses a target. When 

74 neither target is present, prefer the universal .agents/skills target. 

75 """ 

76 targets = get_all_target_dirs(project_root) 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

77 by_name = {target.name: target for target in targets} 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

78 selected: list[InstallTarget] = [] 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

79 

80 if (project_root / ".agents").exists(): 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

81 selected.append(by_name["universal"]) 2p j m a ubE F q k n b ybG H r l o c CbI J

82 if (project_root / ".claude").exists(): 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

83 selected.append(by_name["claude-compatible"]) 2p ubadbdq ybcdddr Cbedfd

84 

85 if not selected: 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

86 selected.append(by_name["universal"]) 2y d T : W X Y Rb; a ubE F z e U = Z 0 1 Sb? b ybG H A f V @ 2 3 4 Tb[ c CbI J

87 return selected 2y p d j T : m W X Y Rb; a ubE F z q e k U = n Z 0 1 Sb? b ybG H A r f l V @ o 2 3 4 Tb[ c CbI J

88 

89 

90def get_existing_target_dirs( 1gsthuviwx

91 project_root: Path, *, include_claude: bool = False 

92) -> list[InstallTarget]: 

93 """Get targets to inspect for existing installed skills. 

94 

95 Without explicit Claude compatibility, this only returns concrete skills 

96 directories that already exist. With include_claude, preserve the legacy 

97 explicit behavior of managing both known targets. 

98 """ 

99 if include_claude: 29b!b] - #by p d j T : m ~b^ _ ac` bc; 8 9 ! { cca 5 K B ^bFbGb$b%b| . 'bz q e k U = n dc} ~ ecabfc? # $ % bbgcb 6 L C _bHbIb(b)bcb/ *bA r f l V @ o hcdbebicfbjc[ ' ( ) gbkcc 7 M D `bJbKb

100 return get_target_dirs(project_root, include_claude=True) 2^bgdhd_bidjd`bkdld

101 return [ 29b!b] - #by p d j T : m ~b^ _ ac` bc; 8 9 ! { cca 5 K B ^bFbGb$b%b| . 'bz q e k U = n dc} ~ ecabfc? # $ % bbgcb 6 L C _bHbIb(b)bcb/ *bA r f l V @ o hcdbebicfbjc[ ' ( ) gbkcc 7 M D `bJbKb

102 target for target in get_all_target_dirs(project_root) if target.path.is_dir() 

103 ] 

104 

105 

106class InstallError(Exception): 1gsthuviwx

107 """Raised when a skill cannot be installed safely.""" 

108 

109 

110class ToolSkillError(Exception): 1gsthuviwx

111 """Raised when the tool skill cannot be managed safely.""" 

112 

113 

114def install_skill( 1gsthuviwx

115 skill: Skill, 

116 target_dir: Path, 

117 *, 

118 copy: bool = False, 

119) -> Path: 

120 """Install a single skill to the target directory. 

121 

122 Returns the path to the installed skill directory. 

123 """ 

124 dest = target_dir / skill.name 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb

125 source = skill.skill_dir.resolve() 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb

126 

127 if dest.exists() or dest.is_symlink(): 2y p d 6bW X Y lcLbtbK B mc* ncucUbXbnbvbwbz q e 7bZ 0 1 ocMbxbL C pc+ qcvcVbYbobzbAbA r f 8b2 3 4 rcNbBbM D sc, tcwcWbZbpbDbEb

128 if dest.is_symlink(): 2lcK B ncucUbFbGbocL C qcvcVbHbIbrcM D tcwcWbJbKb

129 dest.unlink() 2K B UbFbGbL C VbHbIbM D WbJbKb

130 elif dest.is_file(): 2lcncucYcZcocqcvc0c1crctcwc2c3c

131 raise InstallError(f"Cannot overwrite non-symlink file: {dest}") 2ucmdndvcodpdwcqdrd

132 elif dest.is_dir(): 2lcncYcZcocqc0c1crctc2c3c

133 raise InstallError(f"Cannot overwrite non-symlink directory: {dest}") 2lcncYcZcocqc0c1crctc2c3c

134 

135 dest.parent.mkdir(parents=True, exist_ok=True) 2y p d 6bW X Y LbtbK B mc* UbXbnbvbwbz q e 7bZ 0 1 MbxbL C pc+ VbYbobzbAbA r f 8b2 3 4 NbBbM D sc, WbZbpbDbEb

136 

137 if copy: 2y p d 6bW X Y LbtbK B mc* UbXbnbvbwbz q e 7bZ 0 1 MbxbL C pc+ VbYbobzbAbA r f 8b2 3 4 NbBbM D sc, WbZbpbDbEb

138 shutil.copytree(source, dest) 2y 6bmc4c5cz 7bpc6c7cA 8bsc8c9c

139 else: 

140 link_target = _get_symlink_target(source=source, dest=dest) 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb

141 try: 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb

142 dest.symlink_to(link_target, target_is_directory=True) 2p d W X Y LbtbK B * UbXbnbvbwbq e Z 0 1 MbxbL C + VbYbobzbAbr f 2 3 4 NbBbM D , WbZbpbDbEb

143 except OSError as e: 2Xb!c#cYb$c%cZb'c(c

144 raise InstallError( 2Xb!c#cYb$c%cZb'c(c

145 f"Could not create symlink: {e}. " 

146 "Use --copy if your system does not support symlinks." 

147 ) from e 

148 

149 return dest 2y p d 6bW X Y LbtbK B mc* Ubnb4cvb5cwbz q e 7bZ 0 1 MbxbL C pc+ Vbob6czb7cAbA r f 8b2 3 4 NbBbM D sc, Wbpb8cDb9cEb

150 

151 

152def _get_symlink_target(*, source: Path, dest: Path) -> Path | str: 1gsthuviwx

153 try: 2p d W X Y LbtbK B Jc)c* UbXbnbvbwbq e Z 0 1 MbxbL C Kc*c+ VbYbobzbAbr f 2 3 4 NbBbM D Lc+c, WbZbpbDbEb

154 return os.path.relpath(source, start=dest.parent.resolve()) 2p d W X Y LbtbK B Jc)c* UbXbnbvbwbq e Z 0 1 MbxbL C Kc*c+ VbYbobzbAbr f 2 3 4 NbBbM D Lc+c, WbZbpbDbEb

155 except ValueError: 2Jc,c-cKc.c/cLc:c;c

156 return source 2Jc,c-cKc.c/cLc:c;c

157 

158 

159def uninstall_skill(skill_name: str, target_dir: Path) -> bool: 1gsthuviwx

160 """Remove an installed symlink. Returns True if something was removed.""" 

161 dest = target_dir / skill_name 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc

162 if dest.is_symlink(): 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc

163 dest.unlink() 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc

164 return True 28 9 ! 5 nbMcxcyc# $ % 6 obNczcAc' ( ) 7 pbOcBcCc

165 return False 2nbsdtdobudvdpbwdxd

166 

167 

168def get_tool_skill_template() -> str: 1gsthuviwx

169 """Return the bundled Library Skills tool skill template.""" 

170 return ( 1djaQNEFekbROGHflcSPIJ

171 resources.files("library_skills.tool_skill") 

172 .joinpath("SKILL.md") 

173 .read_text(encoding="utf-8") 

174 ) 

175 

176 

177def get_tool_skill_version() -> str: 1gsthuviwx

178 """Return the installed library-skills package version for marker metadata.""" 

179 try: 2d j a Q N PcE F e k b R O QcG H f l c S P RcI J

180 return metadata.version("library-skills") 2d j a Q N PcE F e k b R O QcG H f l c S P RcI J

181 except metadata.PackageNotFoundError: 2Pc=c?cQc@c[cRc]c^c

182 return "0.0.0" 2Pc=c?cQc@c[cRc]c^c

183 

184 

185def inspect_tool_skill(target_dir: Path) -> ToolSkillStatus: 1gsthuviwx

186 """Inspect the copied tool skill status for a target directory.""" 

187 target = _target_for_path(target_dir) 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

188 dest = target_dir / TOOL_SKILL_NAME 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

189 marker = dest / TOOL_SKILL_MARKER 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

190 skill_md = dest / "SKILL.md" 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

191 

192 if not dest.exists() and not dest.is_symlink(): 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

193 return ToolSkillStatus(target=target, path=dest, status="tool skill: missing") 2- y p d j T a {bQ N /b:b. z q e k U b |bR O =b?b/ A r f l V c }bS P [b]b

194 

195 if dest.is_symlink() or not dest.is_dir(): 2m a .bhbObQ ibqbN E F n b ;bjbPbR kbrbO G H o c @blbQbS mbsbP I J

196 return ToolSkillStatus( 2.bydzd;bAdBd@bCdDd

197 target=target, 

198 path=dest, 

199 status="tool skill: blocked by hand-authored directory", 

200 ) 

201 

202 if not _is_managed_tool_skill_marker(marker): 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J

203 return ToolSkillStatus( 2m ibqbScEdTcFdn kbrbUcGdVcHdo mbsbWcIdXcJd

204 target=target, 

205 path=dest, 

206 status="tool skill: invalid marker" 

207 if marker.exists() 

208 else "tool skill: blocked by hand-authored directory", 

209 ) 

210 

211 if not skill_md.is_file(): 2a hbObQ N E F b jbPbR O G H c lbQbS P I J

212 return ToolSkillStatus(target=target, path=dest, status="tool skill: stale") 2ObKdLdPbMdNdQbOdPd

213 

214 try: 2a hbQ N E F b jbR O G H c lbS P I J

215 current = skill_md.read_text(encoding="utf-8") 2a hbQ N E F b jbR O G H c lbS P I J

216 except (OSError, UnicodeDecodeError): 2hb_c`cjb{c|clb}c~c

217 return ToolSkillStatus(target=target, path=dest, status="tool skill: stale") 2hb_c`cjb{c|clb}c~c

218 

219 status = ( 1aQNEFbROGHcSPIJ

220 "tool skill: up to date" 

221 if current == get_tool_skill_template() 

222 else "tool skill: stale" 

223 ) 

224 return ToolSkillStatus(target=target, path=dest, status=status) 1aQNEFbROGHcSPIJ

225 

226 

227def install_tool_skill(target_dir: Path) -> Path: 1gsthuviwx

228 """Install or update the copied Library Skills tool skill in a target.""" 

229 status = inspect_tool_skill(target_dir) 2d j m a Q ibqbN E F e k n b R kbrbO G H f l o c S mbsbP I J

230 if status.status in { 2d j m a Q ibqbN E F e k n b R kbrbO G H f l o c S mbsbP I J

231 "tool skill: blocked by hand-authored directory", 

232 "tool skill: invalid marker", 

233 }: 

234 raise ToolSkillError(f"Cannot overwrite {status.status}: {status.path}") 2m ibqbScTcn kbrbUcVco mbsbWcXc

235 

236 dest = target_dir / TOOL_SKILL_NAME 1djaQNEFekbROGHflcSPIJ

237 if dest.exists(): 1djaQNEFekbROGHflcSPIJ

238 shutil.rmtree(dest) 1aNEFbOGHcPIJ

239 dest.mkdir(parents=True, exist_ok=True) 1djaQNEFekbROGHflcSPIJ

240 (dest / "SKILL.md").write_text(get_tool_skill_template(), encoding="utf-8") 1djaQNEFekbROGHflcSPIJ

241 (dest / TOOL_SKILL_MARKER).write_text( 1djaQNEFekbROGHflcSPIJ

242 json.dumps( 

243 {"kind": TOOL_SKILL_KIND, "version": get_tool_skill_version()}, 

244 indent=2, 

245 ) 

246 + "\n", 

247 encoding="utf-8", 

248 ) 

249 return dest 1djaQNEFekbROGHflcSPIJ

250 

251 

252def _target_for_path(target_dir: Path) -> InstallTarget: 1gsthuviwx

253 if target_dir.as_posix().endswith(CLAUDE_SKILLS_DIR): 2- y p d j T m a {b.bhbObQ ibqbN /b:b. z q e k U n b |b;bjbPbR kbrbO =b?b/ A r f l V o c }b@blbQbS mbsbP [b]b

254 return InstallTarget(name="claude-compatible", path=target_dir) 2- {b/b:b. |b=b?b/ }b[b]b

255 return InstallTarget(name="universal", path=target_dir) 2- y p d j T m a .bhbObQ ibqbN /b:b. z q e k U n b ;bjbPbR kbrbO =b?b/ A r f l V o c @blbQbS mbsbP [b]b

256 

257 

258def _is_managed_tool_skill_marker(marker: Path) -> bool: 1gsthuviwx

259 try: 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J

260 data = json.loads(marker.read_text(encoding="utf-8")) 2m a hbObQ ibqbN E F n b jbPbR kbrbO G H o c lbQbS mbsbP I J

261 except (OSError, UnicodeDecodeError, json.JSONDecodeError): 2m ibScTcn kbUcVco mbWcXc

262 return False 2m ibScTcn kbUcVco mbWcXc

263 return isinstance(data, dict) and data.get("kind") == TOOL_SKILL_KIND 2a hbObQ qbN E F b jbPbR rbO G H c lbQbS sbP I J

264 

265 

266def list_installed_skills(target_dir: Path) -> list[InstalledSkill]: 1gsthuviwx

267 """List skills currently installed in the target directory.""" 

268 results: list[InstalledSkill] = [] 29b!b] - #by p d j T : m 0b^ _ ` ; 8 9 ! { a 5 K B * 1b+bFbGb$b%b| . 'bz q e k U = n 2b} ~ ab? # $ % bbb 6 L C + 3b,bHbIb(b)bcb/ *bA r f l V @ o 4bdbebfb[ ' ( ) gbc 7 M D , 5b-bJbKb

269 

270 if not target_dir.is_dir(): 29b!b] - #by p d j T : m 0b^ _ ` ; 8 9 ! { a 5 K B * 1b+bFbGb$b%b| . 'bz q e k U = n 2b} ~ ab? # $ % bbb 6 L C + 3b,bHbIb(b)bcb/ *bA r f l V @ o 4bdbebfb[ ' ( ) gbc 7 M D , 5b-bJbKb

271 return results 29b!b- #by p d T : ; a B 1bQdRd$b%b. 'bz q e U = ? b C 3bSdTd(b)b/ *bA r f V @ [ c D 5bUdVd

272 

273 for child in sorted(target_dir.iterdir()): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb

274 if not child.is_dir() and not child.is_symlink(): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb

275 continue 21bWdXd3bYdZd5b0d1d

276 skill_md = child / "SKILL.md" 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb

277 if child.is_symlink(): 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb

278 install_type = "symlink" 2] j 0b^ _ ` 8 9 ! { 5 K B * +bFbGb| k 2b} ~ ab# $ % bb6 L C + ,bHbIbcbl 4bdbebfb' ( ) gb7 M D , -bJbKb

279 target = child.resolve() 2] j 0b^ _ ` 8 9 ! { 5 K B * +bFbGb| k 2b} ~ ab# $ % bb6 L C + ,bHbIbcbl 4bdbebfb' ( ) gb7 M D , -bJbKb

280 else: 

281 install_type = "directory" 2m 0ba 5 1bxcycn 2bb 6 3bzcAco 4bc 7 5bBcCc

282 target = None 2m 0ba 5 1bxcycn 2bb 6 3bzcAco 4bc 7 5bBcCc

283 

284 results.append( 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb

285 InstalledSkill( 

286 name=child.name, 

287 type=install_type, 

288 path=child, 

289 target=target, 

290 has_skill_md=skill_md.is_file(), 

291 ) 

292 ) 

293 

294 return results 2] j m 0b^ _ ` 8 9 ! { a 5 K B * 1b+bFbGb| k n 2b} ~ ab# $ % bbb 6 L C + 3b,bHbIbcbl o 4bdbebfb' ( ) gbc 7 M D , 5b-bJbKb