Coverage for src / library_skills / cli.py: 100%
439 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
1import json 1Q23R45S67
2from collections import Counter 1Q23R45S67
3from dataclasses import dataclass 1Q23R45S67
4from pathlib import Path 1Q23R45S67
5from typing import Annotated 1Q23R45S67
7import typer 1Q23R45S67
8from rich_toolkit import RichToolkit 1Q23R45S67
9from rich_toolkit.menu import Menu, Option 1Q23R45S67
11from . import ui 1Q23R45S67
12from .deps import ( 1Q23R45S67
13 get_node_workspace_top_level_deps,
14 get_top_level_deps,
15 get_workspace_top_level_deps,
16)
17from .installer import ( 1Q23R45S67
18 TOOL_SKILL_MARKER,
19 TOOL_SKILL_NAME,
20 InstallError,
21 InstallTarget,
22 ToolSkillError,
23 ToolSkillStatus,
24 get_all_target_dirs,
25 get_default_install_target_dirs,
26 get_existing_target_dirs,
27 get_target_dirs,
28 inspect_tool_skill,
29 install_skill,
30 install_tool_skill,
31 list_installed_skills,
32 uninstall_skill,
33)
34from .python_env import ( 1Q23R45S67
35 find_node_modules,
36 find_project_root,
37 find_venv,
38 get_site_packages_dir,
39)
40from .scanner import ( 1Q23R45S67
41 ScanResult,
42 Skill,
43 _normalize_package_name,
44 scan_node_packages,
45 scan_python_distributions,
46)
47from .workspace import ( 1Q23R45S67
48 NodeWorkspace,
49 UvWorkspace,
50 find_node_workspace,
51 find_uv_workspace,
52 node_workspace_dependency_files,
53 workspace_dependency_files,
54)
56console = ui.console 1Q23R45S67
58app = typer.Typer( 1Q23R45S67
59 name="library-skills",
60 help="Discover and reconcile agent skills from installed library packages.",
61 invoke_without_command=True,
62)
65@dataclass(frozen=True) 1Q23R45S67
66class ProjectContext: 1Q23R45S67
67 cwd: Path 1QRS
68 project_root: Path 1QRS
69 target_environment: Path | None 1QRS
70 site_packages_dir: Path | None 1QRS
71 node_modules_dir: Path | None 1QRS
72 workspace: UvWorkspace | None = None 1Q23R45S67
73 node_workspace: NodeWorkspace | None = None 1Q23R45S67
74 workspace_dependency_files: tuple[Path, ...] = () 1Q23R45S67
75 node_workspace_dependency_files: tuple[Path, ...] = () 1Q23R45S67
76 dependency_files: tuple[Path, ...] = () 1Q23R45S67
79@dataclass(frozen=True) 1Q23R45S67
80class InstalledStatus: 1Q23R45S67
81 target: InstallTarget 1QRS
82 name: str 1QRS
83 type: str 1QRS
84 path: Path 1QRS
85 target_path: Path | None 1QRS
86 status: str 1QRS
87 skill: Skill | None = None 1Q23R45S67
90def _get_rich_toolkit() -> RichToolkit: 1Q23R45S67
91 return ui.get_toolkit(console=console) 2FeAcBcCcGeDcEcFcHeGcHcIc
94def _get_project_context(cwd: Path | None = None) -> ProjectContext: 1Q23R45S67
95 cwd = cwd or Path.cwd() 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
96 workspace = find_uv_workspace(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
97 node_workspace = find_node_workspace(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
98 project_root = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
99 workspace.root
100 if workspace is not None
101 else node_workspace.root
102 if node_workspace is not None
103 else find_project_root(cwd) or cwd
104 )
105 workspace_files = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
106 tuple(workspace_dependency_files(workspace)) if workspace is not None else ()
107 )
108 node_workspace_files = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
109 tuple(node_workspace_dependency_files(node_workspace))
110 if node_workspace is not None
111 else ()
112 )
113 target_environment = find_venv(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
114 site_packages_dir = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
115 get_site_packages_dir(target_environment) if target_environment else None
116 )
117 node_modules_dir = find_node_modules(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
118 return ProjectContext( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
119 cwd=cwd,
120 project_root=project_root,
121 target_environment=target_environment,
122 site_packages_dir=site_packages_dir,
123 node_modules_dir=node_modules_dir,
124 workspace=workspace,
125 node_workspace=node_workspace,
126 workspace_dependency_files=workspace_files,
127 node_workspace_dependency_files=node_workspace_files,
128 dependency_files=workspace_files + node_workspace_files,
129 )
132def _scan_context(context: ProjectContext) -> ScanResult: 1Q23R45S67
133 result = ScanResult(environment_path=context.target_environment) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
134 if context.site_packages_dir is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
135 python_result = scan_python_distributions(context.site_packages_dir) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb
136 result.skills.extend(python_result.skills) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb
137 result.warnings.extend(python_result.warnings) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb
138 if context.workspace and context.workspace.current_member is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
139 member_venv = context.workspace.current_member / ".venv" 2RbT ib6b7bTbU kb8b9bVbV mb!b#b
140 if member_venv.exists(): 2RbT ib6b7bTbU kb8b9bVbV mb!b#b
141 result.warnings.append( 2T 6b7bU 8b9bV !b#b
142 f"Ignoring member-local .venv in uv workspace: {member_venv}"
143 )
144 if context.node_modules_dir is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
145 node_result = scan_node_packages(context.node_modules_dir) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c
146 result.skills.extend(node_result.skills) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c
147 result.warnings.extend(node_result.warnings) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c
148 if context.site_packages_dir is None and context.node_modules_dir is None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
149 result.warnings.append( 28 Mbdded9 Obfdgd! Qbhdid
150 "No target Python environment with site-packages or node_modules was "
151 "found. Run from a project root after installing dependencies, for "
152 "example with 'uv sync' for Python or 'npm install' for Node.js."
153 )
154 result.environment_path = context.target_environment 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
155 return result 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
158def _print_warnings(warnings: list[str]) -> None: 1Q23R45S67
159 ui.print_warnings(warnings, console=console) 2W X K N 8 h e b k C G n bb* + , } XbYbhb] ^ LbSbI nbzb? Mbz T ibs u v AbBbY Z L O 9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbUbJ obCb@ ObA U kbt w x DbEb0 1 M P ! g d a j B E m db: ; = ab1b2blb{ | PbWbF pbFb[ Qby V mbp q r GbHb
162def _display_path(path: Path | None, project_root: Path) -> str: 1Q23R45S67
163 if path is None: 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb
164 return "" 2%cn s ocpc'co t qcrc(cm p sctc
165 absolute_path = path if path.is_absolute() else Path.cwd() / path 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb
166 try: 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb
167 return str(absolute_path.relative_to(project_root.resolve())) 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb
168 except ValueError: 2%c-d.d'c/d:d(c;d=d
169 return str(path) 2%c-d.d'c/d:d(c;d=d
172def _target_prompt_name(target: InstallTarget) -> str: 1Q23R45S67
173 if target.name == "universal": 2AcJcBcCcDcKcEcFcGcLcHcIc
174 return "Agents (.agents/skills)" 2AcJcBcCcDcKcEcFcGcLcHcIc
175 if target.name == "claude-compatible": 2AcJcBcCcDcKcEcFcGcLcHcIc
176 return "Claude Code (.claude/skills)" 2AcJcBcCcDcKcEcFcGcLcHcIc
177 return target.name 2JcpdqdKcrdsdLctdud
180def _target_short_name(target: InstallTarget) -> str: 1Q23R45S67
181 if target.name == "universal": 2~b1cJc2c3cac4cKc5c6cbc7cLc8c9c
182 return "Agents" 2~b1cJc2c3cac4cKc5c6cbc7cLc8c9c
183 if target.name == "claude-compatible": 2JcpdqdKcrdsdLctdud
184 return "Claude Code" 2JcpdqdKcrdsdLctdud
185 return target.name 2JcpdqdKcrdsdLctdud
188def _print_context(context: ProjectContext) -> None: 1Q23R45S67
189 rows = [("Project root", str(context.project_root))] 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
190 if context.workspace is not None: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
191 rows.append(("Workspace root", str(context.workspace.root))) 2ibKdLdkbMdNdmbOdPd
192 if context.workspace.current_member is not None: 2ibKdLdkbMdNdmbOdPd
193 rows.append(("Workspace member", str(context.workspace.current_member))) 2ibKdLdkbMdNdmbOdPd
194 elif context.node_workspace is not None: 2W X K N 8 h e b k C G n I nbzb? Mbz s u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA t w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby p q r GbHb
195 rows.append(("Workspace root", str(context.node_workspace.root))) 2nb|c}cob~cadpbbdcd
196 if context.node_workspace.current_member is not None: 2nb|c}cob~cadpbbdcd
197 rows.append( 2nb|c}cob~cadpbbdcd
198 ("Workspace member", str(context.node_workspace.current_member))
199 )
200 if context.target_environment: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
201 rows.append( 2W X K N h e b k C G n I zb? z ibs u v AbBbY Z L O i f c l D H o J Cb@ A kbt w x DbEb0 1 M P g d a j B E m F Fb[ y mbp q r GbHb
202 (
203 "Target Python environment",
204 _display_path(context.target_environment, context.project_root),
205 )
206 )
207 else:
208 rows.append(("Target Python environment", "not found")) 28 nbMb|c}c9 obOb~cad! pbQbbdcd
209 if context.site_packages_dir: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
210 rows.append( 2W X K N h e b k C G n I zb? z ibs u v AbBbY Z L O i f c l D H o J Cb@ A kbt w x DbEb0 1 M P g d a j B E m F Fb[ y mbp q r GbHb
211 (
212 "Site-packages",
213 _display_path(context.site_packages_dir, context.project_root),
214 )
215 )
216 if context.node_modules_dir: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
217 rows.append( 2nb? |c}cob@ ~cadpb[ bdcd
218 (
219 "node_modules",
220 _display_path(context.node_modules_dir, context.project_root),
221 )
222 )
223 ui.print_title("context", console=console) 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
224 ui.print_table(ui.context_table(rows), console=console) 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb
227def _print_skills_table(skills: list[Skill]) -> None: 1Q23R45S67
228 """Print a formatted table of discovered skills."""
229 if not skills: 2Sbnbzb? MbibAbBbUbobCb@ ObkbDbEbWbpbFb[ QbmbGbHb
230 ui.print_message("No skills found in installed packages.", console=console) 2MbddedObfdgdQbhdid
231 return 2MbddedObfdgdQbhdid
233 ui.print_table( 2Sbnbzb? ibAbBbUbobCb@ kbDbEbWbpbFb[ mbGbHb
234 ui.skills_table(
235 [
236 (
237 skill.name,
238 skill.package_name,
239 skill.package_version,
240 skill.description,
241 )
242 for skill in skills
243 ]
244 ),
245 console=console,
246 )
249def _top_level_skills( 1Q23R45S67
250 *,
251 context: ProjectContext,
252 skills: list[Skill],
253 include_all: bool,
254) -> list[Skill]:
255 """Filter discovered skills to top-level project dependencies by default."""
256 if include_all: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb
257 return skills 2G bb} Xb$bzb? T +bAbBbH cb~ Zb%bCb@ U /bDbEbE dbab1b'bFb[ V ?bGbHb
259 top_level_deps = get_top_level_deps(context.project_root) 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb
260 if context.workspace is not None: 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb
261 top_level_deps = get_workspace_top_level_deps( 2Rb3bT [b+bib6b7bTb4bU ^b/bkb8b9bVb5bV `b?bmb!b#b
262 list(context.workspace_dependency_files)
263 )
264 if context.node_workspace is not None: 2Rb3bT [b+bib6b7bTb4bU ^b/bkb8b9bVb5bV `b?bmb!b#b
265 node_top_level_deps = get_node_workspace_top_level_deps( 2Rb3b[c]cTb4b^c_cVb5b`c{c
266 list(context.node_workspace_dependency_files)
267 )
268 if node_top_level_deps is not None: 2Rb3b[c]cTb4b^c_cVb5b`c{c
269 top_level_deps = ( 2Rb3b[c]cTb4b^c_cVb5b`c{c
270 node_top_level_deps
271 if top_level_deps is None
272 else top_level_deps | node_top_level_deps
273 )
274 elif context.node_workspace is not None: 2W X K N 8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz s u v AbBbY Z L O 9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA t w x DbEb0 1 M P ! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby p q r GbHb
275 top_level_deps = get_node_workspace_top_level_deps( 2(b)b*b$bnb|c}c,b-b.b%bob~cad:b;b=b'bpbbdcd
276 list(context.node_workspace_dependency_files)
277 )
278 if top_level_deps is None: 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb
279 return skills 28 Mbdded9 Obfdgd! Qbhdid
281 return [ 2W X K N Rb3bh e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ y V `b?bmbp q r GbHb
282 skill
283 for skill in skills
284 if _normalize_package_name(skill.package_name) in top_level_deps
285 ]
288def _scan_json_payload( 1Q23R45S67
289 *, context: ProjectContext, skills: list[Skill], result: ScanResult
290) -> dict[str, object]:
291 return { 2Rb3beb(b)b*b$b? T [b+b6b7bTb4bfb,b-b.b%b@ U ^b/b8b9bVb5bgb:b;b=b'b[ V `b?b!b#b
292 "project_root": str(context.project_root),
293 "workspace_root": str(_workspace_root(context)),
294 "workspace_member": (
295 str(context.workspace.current_member)
296 if context.workspace and context.workspace.current_member
297 else str(context.node_workspace.current_member)
298 if context.node_workspace and context.node_workspace.current_member
299 else ""
300 ),
301 "dependency_files": [str(path) for path in context.dependency_files],
302 "target_environment": str(context.target_environment or ""),
303 "node_modules": str(context.node_modules_dir or ""),
304 "skills": [
305 {
306 "name": skill.name,
307 "description": skill.description,
308 "package": skill.package_name,
309 "version": skill.package_version,
310 "path": str(skill.skill_dir),
311 }
312 for skill in skills
313 ],
314 "warnings": result.warnings,
315 }
318def _workspace_root(context: ProjectContext) -> Path | str: 1Q23R45S67
319 if context.workspace is not None: 2Rb3beb(b)b*b$b? T [b+b6b7bTb4bfb,b-b.b%b@ U ^b/b8b9bVb5bgb:b;b=b'b[ V `b?b!b#b
320 return context.workspace.root 2Rb3bT [b+b6b7bTb4bU ^b/b8b9bVb5bV `b?b!b#b
321 if context.node_workspace is not None: 2eb(b)b*b$b? ?d@dfb,b-b.b%b@ [d]dgb:b;b=b'b[ ^d_d
322 return context.node_workspace.root 2(b)b*b$b?d@d,b-b.b%b[d]d:b;b=b'b^d_d
323 return "" 2eb? IeJefb@ KeLegb[ MeNe
326def _print_action_header(action: str) -> None: 1Q23R45S67
327 ui.print_action_header(action, console=console) 2~b1c2c3cac4c5c6cbc7c8c9c
330def _select_skills_interactive(skills: list[Skill]) -> list[Skill]: 1Q23R45S67
331 """Let the user interactively select which skills to install."""
332 _print_action_header("install") 21c2c3c4c5c6c7c8c9c
333 return _get_rich_toolkit().ask( 21c2c3c4c5c6c7c8c9c
334 "Select skills to install (press Space to select, Enter to confirm):",
335 options=[
336 Option({"name": f"{skill.name} ({skill.package_name})", "value": skill})
337 for skill in skills
338 ],
339 allow_filtering=True,
340 multiple=True,
341 )
344def _select_targets_interactive( 1Q23R45S67
345 *, project_root: Path, default_targets: list[InstallTarget]
346) -> list[InstallTarget]:
347 """Let the user interactively select which targets to install into."""
348 targets = get_all_target_dirs(project_root) 2AcBcCcDcEcFcGcHcIc
349 default_names = {target.name for target in default_targets} 2AcBcCcDcEcFcGcHcIc
350 toolkit = _get_rich_toolkit() 2AcBcCcDcEcFcGcHcIc
351 menu = Menu( 2AcBcCcDcEcFcGcHcIc
352 "Select installation targets (press Space to select, Enter to confirm):",
353 options=[
354 Option(
355 {
356 "name": _target_prompt_name(target),
357 "value": target,
358 }
359 )
360 for target in targets
361 ],
362 style=toolkit.style,
363 console=toolkit.console,
364 multiple=True,
365 )
366 menu.checked = { 2AcBcCcDcEcFcGcHcIc
367 index for index, target in enumerate(targets) if target.name in default_names
368 }
369 return menu.ask() 2AcBcCcDcEcFcGcHcIc
372def _select_install_targets( 1Q23R45S67
373 *,
374 project_root: Path,
375 include_claude: bool,
376 interactive: bool,
377) -> list[InstallTarget]:
378 if not interactive: 2h e b G bb* + , } XbT 6b7bi f c H cb- . / ~ ZbU 8b9bg d a E db: ; = ab1bV !b#b
379 return get_target_dirs(project_root, include_claude=include_claude) 2bb} T 6b7bcb~ U 8b9bdbabV !b#b
380 return _select_targets_interactive( 2h e b G * + , Xb`d{di f c H - . / Zb|d}dg d a E : ; = 1b~dae
381 project_root=project_root,
382 default_targets=get_default_install_target_dirs(project_root),
383 )
386def _select_tool_skill_interactive() -> bool: 1Q23R45S67
387 toolkit = _get_rich_toolkit() 2vdwdxdydzdAdBdCdDd
388 menu = Menu( 2vdwdxdydzdAdBdCdDd
389 "Copy the Library Skills tool skill into the project so agents know how "
390 "to update, repair, and check managed skills?",
391 options=[
392 Option(
393 {
394 "name": "Copy Library Skills tool skill",
395 "value": True,
396 }
397 )
398 ],
399 style=toolkit.style,
400 console=toolkit.console,
401 multiple=True,
402 )
403 menu.checked = {0} 2vdwdxdydzdAdBdCdDd
404 return True in menu.ask() 2vdwdxdydzdAdBdCdDd
407def _find_collisions(skills: list[Skill]) -> set[str]: 1Q23R45S67
408 counts = Counter(skill.name for skill in skills) 2W X K N 8 h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bY Z L O 9 i f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9b0 1 M P ! g d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
409 return {name for name, count in counts.items() if count > 1} 2W X K N 8 h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bY Z L O 9 i f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9b0 1 M P ! g d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
412def _deduplicate_skills(skills: list[Skill]) -> list[Skill]: 1Q23R45S67
413 seen: set[Path] = set() 2h e b icjci f c kclcg d a mcnc
414 unique: list[Skill] = [] 2h e b icjci f c kclcg d a mcnc
415 for skill in skills: 2h e b icjci f c kclcg d a mcnc
416 key = skill.skill_dir.resolve() 2h e b icjci f c kclcg d a mcnc
417 if key in seen: 2h e b icjci f c kclcg d a mcnc
418 continue 2e OePef QeRed SeTe
419 seen.add(key) 2h e b icjci f c kclcg d a mcnc
420 unique.append(skill) 2h e b icjci f c kclcg d a mcnc
421 return unique 2h e b icjci f c kclcg d a mcnc
424def _filter_installable_skills( 1Q23R45S67
425 skills: list[Skill],
426 *,
427 selected_names: list[str],
428 include_all: bool,
429) -> list[Skill]:
430 collisions = _find_collisions(skills) 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
431 if collisions: 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
432 ui.print_warning( 2!cUeVe#cWeXe$cYeZe
433 "Skipping colliding skill names: " + ", ".join(sorted(collisions)),
434 console=console,
435 )
437 installable = [skill for skill in skills if skill.name not in collisions] 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
438 if selected_names: 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b
439 selected = set(selected_names) 2} T 6b7b~ U 8b9babV !b#b
440 return [skill for skill in installable if skill.name in selected] 2} T 6b7b~ U 8b9babV !b#b
441 if include_all: 2h e b k C G n !cbb* + , XbYbI z s u v # $ i f c l D H o #ccb- . / Zb0bJ A t w x % ' g d a j B E m $cdb: ; = 1b2bF y p q r ( )
442 return installable 2G !cbbXb0e1eH #ccbZb2e3eE $cdb1b4e5e
443 return [] 2h e b k C n * + , YbI z s u v # $ i f c l D o - . / 0bJ A t w x % ' g d a j B m : ; = 2bF y p q r ( )
446def _installed_statuses( 1Q23R45S67
447 *,
448 targets: list[InstallTarget],
449 skills: list[Skill],
450) -> list[InstalledStatus]:
451 skills_by_dir = {skill.skill_dir.resolve(): skill for skill in skills} 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
452 skills_by_name = {skill.name: skill for skill in skills} 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
453 statuses: list[InstalledStatus] = [] 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
455 for target in targets: 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
456 for installed in list_installed_skills(target.path): 2W X K N 8 h e b k C G n {b] ^ ebI qbrbsbIbz s u v # $ Y Z L O 9 i f c l D H o |b_ ` fbJ tbubvbJbA t w x % ' 0 1 M P ! g d a j B E m }b{ | gbF wbxbybKby p q r ( )
457 if (
458 installed.name == TOOL_SKILL_NAME
459 and (installed.path / TOOL_SKILL_MARKER).exists()
460 ):
461 continue 2z ccdcA ecfcy gchc
462 target_path = installed.target 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )
463 skill = skills_by_dir.get(target_path) if target_path else None 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )
465 status = "hand-authored" 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )
466 if installed.type == "symlink": 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )
467 if target_path is None or not target_path.exists(): 2K k {b] ^ ebqbrbsbIbs u v # $ L l |b_ ` fbtbubvbJbt w x % ' M j }b{ | gbwbxbybKbp q r ( )
468 status = "broken" 2K {bs u v # $ L |bt w x % ' M }bp q r ( )
469 skill = skills_by_name.get(installed.name) 2K {bs u v # $ L |bt w x % ' M }bp q r ( )
470 elif skill: 2k {b] ^ ebqbrbsbIbs ocpcl |b_ ` fbtbubvbJbt qcrcj }b{ | gbwbxbybKbp sctc
471 status = ( 2k {b] ^ ebqbrbsbIbQdRdl |b_ ` fbtbubvbJbSdTdj }b{ | gbwbxbybKbUdVd
472 "up to date"
473 if installed.name == skill.name
474 else "name mismatch"
475 )
476 elif installed.name in skills_by_name: 2{bs ocpc|bt qcrc}bp sctc
477 skill = skills_by_name[installed.name] 2{bbece|bdeee}bfege
478 status = "outdated" 2{bbece|bdeee}bfege
479 else:
480 status = "orphaned" 2{bs ocpc|bt qcrc}bp sctc
482 statuses.append( 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )
483 InstalledStatus(
484 target=target,
485 name=installed.name,
486 type=installed.type,
487 path=installed.path,
488 target_path=target_path,
489 status=status,
490 skill=skill,
491 )
492 )
494 installed_names = { 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
495 (status.target.name, status.name)
496 for status in statuses
497 if status.status in {"up to date", "outdated", "broken", "name mismatch"}
498 }
499 for target in targets: 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
500 for skill in skills: 2W X K N 8 h e b k C G n {b] ^ ebI qbrbsbIbz s u v # $ Y Z L O 9 i f c l D H o |b_ ` fbJ tbubvbJbA t w x % ' 0 1 M P ! g d a j B E m }b{ | gbF wbxbybKby p q r ( )
501 if (target.name, skill.name) not in installed_names: 2h e b k G {b] ^ ebqbrbsbIbu v # $ i f c l H |b_ ` fbtbubvbJbw x % ' g d a j E }b{ | gbwbxbybKbq r ( )
502 statuses.append( 1hebGuv#$ifcHwx%'gdaEqr()
503 InstalledStatus(
504 target=target,
505 name=skill.name,
506 type="missing",
507 path=target.path / skill.name,
508 target_path=skill.skill_dir,
509 status="new",
510 skill=skill,
511 )
512 )
513 return statuses 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )
516def _print_status_table(statuses: list[InstalledStatus], project_root: Path) -> None: 1Q23R45S67
517 ui.print_table( 1KhebkGn]^suv#$LifclHo_`twx%'MgdajEm{|pqr()
518 ui.status_table(
519 [
520 (
521 status.target.name,
522 status.name,
523 status.status,
524 _display_path(status.path, project_root),
525 _display_path(status.target_path, project_root),
526 )
527 for status in statuses
528 ]
529 ),
530 console=console,
531 )
534def _print_tool_skill_status_table( 1Q23R45S67
535 statuses: list[ToolSkillStatus], project_root: Path
536) -> None:
537 ui.print_table( 2N b k n z McNcO c l o A OcPcP a j m y QcRc
538 ui.tool_skill_status_table(
539 [
540 (
541 status.target.name,
542 status.status,
543 _display_path(status.path, project_root),
544 )
545 for status in statuses
546 ]
547 ),
548 console=console,
549 )
552def _print_installed_skills_table( 1Q23R45S67
553 statuses: list[InstalledStatus], project_root: Path
554) -> None:
555 installed = [status for status in statuses if status.type != "missing"] 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c
556 if not installed: 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c
557 ui.print_message("No skills installed.", console=console) 2hbLb)c*cjbNb+c,clbPb-c.c
558 return 2hbLb)c*cjbNb+c,clbPb-c.c
559 _print_status_table(installed, project_root) 2] ^ 6e7e_ ` 8e9e{ | !e#e
562def _select_installed_skills_interactive( 1Q23R45S67
563 statuses: list[InstalledStatus],
564) -> list[InstalledStatus]:
565 """Let the user interactively select which installed skills to remove."""
566 removable = [status for status in statuses if status.type == "symlink"] 21c2c3c4c5c6c7c8c9c
567 if not removable: 21c2c3c4c5c6c7c8c9c
568 return [] 21c2c3c4c5c6c7c8c9c
569 return _get_rich_toolkit().ask( 21c2c3c4c5c6c7c8c9c
570 "Select skills to remove (press Space to select, Enter to confirm):",
571 options=[
572 Option(
573 {
574 "name": f"{status.name} [{_target_short_name(status.target)}]",
575 "value": status,
576 }
577 )
578 for status in removable
579 ],
580 allow_filtering=True,
581 multiple=True,
582 )
585def _sync_target_dirs( 1Q23R45S67
586 *, project_root: Path, include_claude: bool, yes: bool, check: bool
587) -> list[InstallTarget]:
588 targets_by_name = { 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
589 target.name: target for target in get_existing_target_dirs(project_root)
590 }
591 default_targets = ( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
592 get_target_dirs(project_root, include_claude=include_claude)
593 if yes or check or include_claude
594 else get_default_install_target_dirs(project_root)
595 )
596 for target in default_targets: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
597 targets_by_name[target.name] = target 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
598 return list(targets_by_name.values()) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
601def _repairable_statuses(statuses: list[InstalledStatus]) -> list[InstalledStatus]: 1Q23R45S67
602 return [ 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
603 status
604 for status in statuses
605 if status.type == "symlink"
606 and status.status in {"broken", "outdated"}
607 and status.skill is not None
608 ]
611def _removable_statuses(statuses: list[InstalledStatus]) -> list[InstalledStatus]: 1Q23R45S67
612 return [ 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
613 status
614 for status in statuses
615 if status.type == "symlink"
616 and (
617 status.status == "orphaned"
618 or (status.status == "broken" and status.skill is None)
619 )
620 ]
623def _select_statuses_interactive( 1Q23R45S67
624 statuses: list[InstalledStatus], *, action: str
625) -> list[InstalledStatus]:
626 if not statuses: 2h e b k C G n I ~bz ccdci f c l D H o J acA ecfcg d a j B E m F bcy gchc
627 return [] 2h e b k C G n I ~bz ccdci f c l D H o J acA ecfcg d a j B E m F bcy gchc
628 _print_action_header(action) 2~b/c:cac;c=cbc?c@c
629 toolkit = _get_rich_toolkit() 2~b/c:cac;c=cbc?c@c
630 menu = Menu( 2~b/c:cac;c=cbc?c@c
631 f"Select skills to {action} (press Space to select, Enter to confirm):",
632 options=[
633 Option(
634 {
635 "name": f"{status.name} [{_target_short_name(status.target)}]",
636 "value": status,
637 }
638 )
639 for status in statuses
640 ],
641 style=toolkit.style,
642 console=toolkit.console,
643 multiple=True,
644 )
645 menu.checked = set(range(len(statuses))) 2~b/c:cac;c=cbc?c@c
646 return menu.ask() 2~b/c:cac;c=cbc?c@c
649def _repair_selected(*, statuses: list[InstalledStatus], project_root: Path) -> int: 1Q23R45S67
650 repaired_count = 0 2~bu v # $ acw x % ' bcq r ( )
651 for status in statuses: 2~bu v # $ acw x % ' bcq r ( )
652 if status.skill is None: 2~bu v # $ acw x % ' bcq r ( )
653 continue 2~b/c:cac;c=cbc?c@c
654 install_skill(status.skill, status.target.path) 1uv#$wx%'qr()
655 ui.print_result( 1uv#$wx%'qr()
656 "Repaired",
657 status.name,
658 status.target.name,
659 _display_path(status.path, project_root),
660 console=console,
661 )
662 repaired_count += 1 1uv#$wx%'qr()
663 return repaired_count 2~bu v # $ acw x % ' bcq r ( )
666def _remove_selected(*, statuses: list[InstalledStatus], project_root: Path) -> int: 1Q23R45S67
667 removed_count = 0 2~bs ocpcact qcrcbcp sctc
668 for status in statuses: 2~bs ocpcact qcrcbcp sctc
669 if uninstall_skill(status.name, status.target.path): 2~bs ocpcact qcrcbcp sctc
670 ui.print_result( 2s ocpct qcrcp sctc
671 f"Removed {status.status} symlink",
672 status.name,
673 status.target.name,
674 _display_path(status.path, project_root),
675 console=console,
676 )
677 removed_count += 1 2s ocpct qcrcp sctc
678 else:
679 ui.print_message( 2~b/c:cac;c=cbc?c@c
680 f"Not found: [bold]{status.name}[/bold] ({status.target.name})",
681 console=console,
682 )
683 return removed_count 2~bs ocpcact qcrcbcp sctc
686def _install_selected( 1Q23R45S67
687 *,
688 skills: list[Skill],
689 targets: list[InstallTarget],
690 project_root: Path,
691 copy: bool = False,
692) -> int:
693 installed_count = 0 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
694 for target in targets: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
695 for skill in skills: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
696 try: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
697 dest = install_skill(skill, target.path, copy=copy) 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
698 except InstallError as e: 2ScWdXdTcYdZdUc0d1d
699 ui.print_skipped(skill.name, str(e), console=console) 2ScWdXdTcYdZdUc0d1d
700 continue 2ScWdXdTcYdZdUc0d1d
701 action = "Copied" if copy else "Installed" 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b
702 ui.print_result( 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b
703 action,
704 skill.name,
705 skill.package_name,
706 _display_path(dest, project_root),
707 console=console,
708 )
709 installed_count += 1 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b
710 if installed_count and not copy: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
711 ui.print_hint( 2e b * + , } T 6b7bf c - . / ~ U 8b9bd a : ; = abV !b#b
712 "These relative symlinks can be committed to Git when your project "
713 "uses stable repo-local installs. They resolve after dependencies "
714 "are installed.",
715 console=console,
716 )
717 return installed_count 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b
720def _sync_tool_skill( 1Q23R45S67
721 *,
722 targets: list[InstallTarget],
723 project_root: Path,
724 check: bool,
725 explicit: bool,
726) -> tuple[int, bool]:
727 statuses = [inspect_tool_skill(target.path) for target in targets] 2N b k n z McNcO c l o A OcPcP a j m y QcRc
728 _print_tool_skill_status_table(statuses, project_root) 2N b k n z McNcO c l o A OcPcP a j m y QcRc
730 drift = [status for status in statuses if status.status != "tool skill: up to date"] 2N b k n z McNcO c l o A OcPcP a j m y QcRc
731 if check: 2N b k n z McNcO c l o A OcPcP a j m y QcRc
732 return 0, bool(drift) 2N McNcO OcPcP QcRc
734 changed_count = 0 2b k n z ccdcc l o A ecfca j m y gchc
735 failed = False 2b k n z ccdcc l o A ecfca j m y gchc
736 for status in drift: 2b k n z ccdcc l o A ecfca j m y gchc
737 try: 2b k n z ccdcc l o A ecfca j m y gchc
738 install_tool_skill(status.target.path) 2b k n z ccdcc l o A ecfca j m y gchc
739 except ToolSkillError as e: 2n jdkdo ldmdm ndod
740 ui.print_skipped("tool skill", str(e), console=console) 2n jdkdo ldmdm ndod
741 failed = failed or explicit 2n jdkdo ldmdm ndod
742 continue 2n jdkdo ldmdm ndod
743 ui.print_result( 2b k z ccdcc l A ecfca j y gchc
744 "Copied",
745 "library-skills",
746 status.target.name,
747 _display_path(status.path, project_root),
748 console=console,
749 )
750 changed_count += 1 2b k z ccdcc l A ecfca j y gchc
751 return changed_count, failed 2b k n z ccdcc l o A ecfca j m y gchc
754def _tool_skill_is_missing(targets: list[InstallTarget]) -> bool: 1Q23R45S67
755 return any( 2h e b k C heiei f c l D jekeg d a j B leme
756 inspect_tool_skill(target.path).status == "tool skill: missing"
757 for target in targets
758 )
761def _sync( 1Q23R45S67
762 *,
763 include_claude: bool,
764 yes: bool,
765 check: bool,
766 include_all: bool,
767 selected_names: list[str],
768 copy: bool,
769 tool_skill: bool | None,
770) -> None:
771 context = _get_project_context() 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
772 result = _scan_context(context) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
773 targets = _sync_target_dirs( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
774 project_root=context.project_root,
775 include_claude=include_claude,
776 yes=yes,
777 check=check,
778 )
780 _print_context(context) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
781 ui.print_line(console=console) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
782 _print_warnings(result.warnings) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
783 if result.warnings: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
784 ui.print_line(console=console) 28 $e%e9 'e(e! )e*e
786 collisions = _find_collisions(result.skills) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
787 candidate_skills = _top_level_skills( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
788 context=context,
789 skills=result.skills,
790 include_all=include_all or bool(selected_names),
791 )
792 candidate_skill_names = {skill.name for skill in candidate_skills} 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
793 statuses = _installed_statuses( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
794 targets=targets,
795 skills=[skill for skill in result.skills if skill.name not in collisions],
796 )
797 visible_statuses = [ 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
798 status
799 for status in statuses
800 if status.status != "new" or status.name in candidate_skill_names
801 ]
803 if visible_statuses: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
804 ui.print_title("status", console=console) 1KhebkGnsuv#$LifclHotwx%'MgdajEmpqr()
805 _print_status_table(visible_statuses, context.project_root) 1KhebkGnsuv#$LifclHotwx%'MgdajEmpqr()
806 else:
807 ui.print_message("No installed or discovered skills found.", console=console) 2W X N 8 C I z neoeY Z O 9 D J A peqe0 1 P ! B F y rese
809 drift = [ 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
810 status
811 for status in statuses
812 if status.status in {"broken", "outdated", "name mismatch", "orphaned"}
813 ]
814 if check: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
815 if tool_skill: 2W X K N 8 2d3dY Z L O 9 4d5d0 1 M P ! 6d7d
816 ui.print_line(console=console) 2N McNcO OcPcP QcRc
817 _, tool_failed = _sync_tool_skill( 2N McNcO OcPcP QcRc
818 targets=get_target_dirs(
819 context.project_root, include_claude=include_claude
820 ),
821 project_root=context.project_root,
822 check=True,
823 explicit=True,
824 )
825 if tool_failed: 2N McNcO OcPcP QcRc
826 raise typer.Exit(1) 2N McNcO OcPcP QcRc
827 if drift or collisions: 2W X K 8 2d3dY Z L 9 4d5d0 1 M ! 6d7d
828 raise typer.Exit(1) 2W K 2d3dY L 4d5d0 M 6d7d
829 return 2X 8 neoeZ 9 peqe1 ! rese
831 repairable = _repairable_statuses(drift) 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
832 removable = _removable_statuses(drift) 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
833 if drift: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
834 ui.print_title("attention", console=console) 1suv#$twx%'pqr()
835 ui.print_warning("Some installed skills need attention.", console=console) 1suv#$twx%'pqr()
836 ui.print_message( 1suv#$twx%'pqr()
837 "Select the skills to install, repair, or remove.", console=console
838 )
839 ui.print_hint("Only managed symlinks will be changed.", console=console) 1suv#$twx%'pqr()
841 selected_repairs = ( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
842 repairable if yes else _select_statuses_interactive(repairable, action="repair")
843 )
844 selected_removals = ( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
845 removable if yes else _select_statuses_interactive(removable, action="remove")
846 )
847 if selected_repairs: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
848 ui.print_line(console=console) 1uv#$wx%'qr()
849 _repair_selected(statuses=selected_repairs, project_root=context.project_root) 1uv#$wx%'qr()
850 if selected_removals: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
851 ui.print_line(console=console) 2s ocpct qcrcp sctc
852 _remove_selected(statuses=selected_removals, project_root=context.project_root) 2s ocpct qcrcp sctc
854 selected = _filter_installable_skills( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
855 candidate_skills,
856 selected_names=selected_names,
857 include_all=include_all,
858 )
859 if ( 1gdajBEmFypqr
860 not selected
861 and not yes
862 and not selected_names
863 and not include_all
864 and tool_skill is not True
865 ):
866 new_skills = [ 2h e b k C I 8d9di f c l D J !d#dg d a j B F $d%d
867 status.skill
868 for status in visible_statuses
869 if status.status == "new" and status.skill is not None
870 ]
871 if new_skills: 2h e b k C I 8d9di f c l D J !d#dg d a j B F $d%d
872 selected = _select_skills_interactive(_deduplicate_skills(new_skills)) 2h e b icjci f c kclcg d a mcnc
874 tool_skill_changes = 0 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
875 if selected: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()
876 targets = _select_install_targets( 2h e b G EdFdi f c H GdHdg d a E IdJd
877 project_root=context.project_root,
878 include_claude=include_claude,
879 interactive=not yes and not include_claude,
880 )
881 if not targets: 2h e b G EdFdi f c H GdHdg d a E IdJd
882 ui.print_message("No installation targets selected.", console=console) 2G EdFdH GdHdE IdJd
883 return 2G EdFdH GdHdE IdJd
884 ui.print_line(console=console) 2h e b icjci f c kclcg d a mcnc
885 installed_count = _install_selected( 2h e b icjci f c kclcg d a mcnc
886 skills=selected,
887 targets=targets,
888 project_root=context.project_root,
889 copy=copy,
890 )
891 ui.print_line(console=console) 2h e b icjci f c kclcg d a mcnc
892 ui.print_summary({"installed skill targets": installed_count}, console=console) 2h e b icjci f c kclcg d a mcnc
893 if tool_skill is True: 1hebkCnIzsuv#$ifclDoJAtwx%'gdajBmFypqr()
894 ui.print_line(console=console) 2n z ccdco A ecfcm y gchc
895 tool_skill_changes, tool_failed = _sync_tool_skill( 2n z ccdco A ecfcm y gchc
896 targets=targets,
897 project_root=context.project_root,
898 check=False,
899 explicit=True,
900 )
901 if tool_failed: 2n z ccdco A ecfcm y gchc
902 raise typer.Exit(1) 2n jdkdo ldmdm ndod
903 elif tool_skill is None and not yes and _tool_skill_is_missing(targets): 1hebkCIsuv#$ifclDJtwx%'gdajBFpqr()
904 if _select_tool_skill_interactive(): 2h e b k C heiei f c l D jekeg d a j B leme
905 ui.print_line(console=console) 2b k QdRdc l SdTda j UdVd
906 tool_skill_changes, _ = _sync_tool_skill( 2b k QdRdc l SdTda j UdVd
907 targets=targets,
908 project_root=context.project_root,
909 check=False,
910 explicit=False,
911 )
912 if ( 1gdajBFypqr
913 not selected_repairs
914 and not selected_removals
915 and not selected
916 and not tool_skill_changes
917 ):
918 ui.print_summary({}, console=console) 2C I 8d9dD J !d#dB F $d%d
921@app.callback() 1Q23R45S67
922def callback( 1Q23R45S67
923 ctx: typer.Context,
924 include_claude: Annotated[
925 bool,
926 typer.Option(
927 "--claude",
928 help="Also install/manage skills in .claude/skills/ alongside .agents/skills/",
929 ),
930 ] = False,
931 yes: Annotated[
932 bool, typer.Option("--yes", "-y", help="Skip confirmation prompts")
933 ] = False,
934 check: Annotated[
935 bool, typer.Option("--check", help="Validate only; exit 1 if installs drift")
936 ] = False,
937 include_all: Annotated[
938 bool,
939 typer.Option("--all", help="Install all newly discovered unmanaged skills"),
940 ] = False,
941 selected_names: Annotated[
942 list[str] | None,
943 typer.Option(
944 "--skill", "-s", help="Install a specific discovered skill by name"
945 ),
946 ] = None,
947 copy: Annotated[
948 bool, typer.Option("--copy", help="Copy files instead of creating symlinks")
949 ] = False,
950 tool_skill: Annotated[
951 bool | None,
952 typer.Option(
953 "--tool-skill/--no-tool-skill",
954 help="Copy the Library Skills tool skill into the project",
955 ),
956 ] = None,
957) -> None:
958 """Discover and reconcile agent skills from installed library packages."""
959 if ctx.invoked_subcommand is None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb
960 _sync( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()
961 include_claude=include_claude,
962 yes=yes,
963 check=check,
964 include_all=include_all,
965 selected_names=selected_names or [],
966 copy=copy,
967 tool_skill=tool_skill,
968 )
971@app.command() 1Q23R45S67
972def scan( 1Q23R45S67
973 json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
974 include_all: Annotated[
975 bool,
976 typer.Option("--all", help="Include skills from transitive dependencies"),
977 ] = False,
978) -> None:
979 """Discover skills in installed packages."""
980 context = _get_project_context() 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb
981 result = _scan_context(context) 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb
982 skills = _top_level_skills( 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb
983 context=context,
984 skills=result.skills,
985 include_all=include_all,
986 )
987 if json_output: 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb
988 payload = _scan_json_payload(context=context, skills=skills, result=result) 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b
989 print(json.dumps(payload, indent=2)) 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b
990 return 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b
992 _print_context(context) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb
993 ui.print_line(console=console) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb
994 _print_warnings(result.warnings) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb
995 if result.warnings: 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb
996 ui.print_line(console=console) 2MbddedObfdgdQbhdid
997 _print_skills_table(skills) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb
1000@app.command("list") 1Q23R45S67
1001def list_cmd( 1Q23R45S67
1002 installed: Annotated[
1003 bool, typer.Option("--installed", help="Only show installed skills")
1004 ] = False,
1005 json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
1006 include_claude: Annotated[
1007 bool,
1008 typer.Option(
1009 "--claude",
1010 help="Also include .claude/skills/ alongside .agents/skills/",
1011 ),
1012 ] = False,
1013 include_all: Annotated[
1014 bool,
1015 typer.Option("--all", help="Include skills from transitive dependencies"),
1016 ] = False,
1017) -> None:
1018 """List discovered or currently installed skills."""
1019 context = _get_project_context() 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1020 result = _scan_context(context) 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1021 skills = _top_level_skills( 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1022 context=context,
1023 skills=result.skills,
1024 include_all=include_all,
1025 )
1026 targets = get_existing_target_dirs( 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1027 context.project_root, include_claude=include_claude
1028 )
1029 statuses = _installed_statuses(targets=targets, skills=result.skills) 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1031 if json_output: 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c
1032 payload = _scan_json_payload(context=context, skills=skills, result=result) 2ebVcWcfbXcYcgbZc0c
1033 payload.update( 2ebVcWcfbXcYcgbZc0c
1034 {
1035 "installed": [
1036 {
1037 "target": status.target.name,
1038 "name": status.name,
1039 "status": status.status,
1040 "path": str(status.path),
1041 "source": str(status.target_path or ""),
1042 }
1043 for status in statuses
1044 if status.type != "missing"
1045 ],
1046 }
1047 )
1048 print(json.dumps(payload, indent=2)) 2ebVcWcfbXcYcgbZc0c
1049 return 2ebVcWcfbXcYcgbZc0c
1051 _print_warnings(result.warnings) 2hb] ^ LbSb)c*cjb_ ` NbUb+c,clb{ | PbWb-c.c
1052 if installed: 2hb] ^ LbSb)c*cjb_ ` NbUb+c,clb{ | PbWb-c.c
1053 _print_installed_skills_table(statuses, context.project_root) 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c
1054 else:
1055 _print_skills_table(skills) 2Sb+e,eUb-e.eWb/e:e
1058@app.command() 1Q23R45S67
1059def install( 1Q23R45S67
1060 include_claude: Annotated[
1061 bool,
1062 typer.Option(
1063 "--claude",
1064 help="Also install skills in .claude/skills/ alongside .agents/skills/",
1065 ),
1066 ] = False,
1067 yes: Annotated[
1068 bool, typer.Option("--yes", "-y", help="Skip interactive selection")
1069 ] = False,
1070 include_all: Annotated[
1071 bool,
1072 typer.Option("--all", help="Install all newly discovered unmanaged skills"),
1073 ] = False,
1074 selected_names: Annotated[
1075 list[str] | None,
1076 typer.Option(
1077 "--skill", "-s", help="Install a specific discovered skill by name"
1078 ),
1079 ] = None,
1080 copy: Annotated[
1081 bool, typer.Option("--copy", help="Copy files instead of creating symlinks")
1082 ] = False,
1083) -> None:
1084 """Install skills from installed packages."""
1085 context = _get_project_context() 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1086 result = _scan_context(context) 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1087 skills = _top_level_skills( 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1088 context=context,
1089 skills=result.skills,
1090 include_all=include_all or bool(selected_names),
1091 )
1093 _print_warnings(result.warnings) 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1094 selected = _filter_installable_skills( 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1095 skills,
1096 selected_names=selected_names or [],
1097 include_all=include_all,
1098 )
1099 if not selected and not yes: 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1100 selected = _select_skills_interactive(skills) 2* + , `d{d- . / |d}d: ; = ~dae
1102 if not selected: 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b
1103 ui.print_message("No skills selected.", console=console) 2Ybteue0bvewe2bxeye
1104 return 2Ybteue0bvewe2bxeye
1106 targets = _select_install_targets( 2bb* + , } XbT 6b7bcb- . / ~ ZbU 8b9bdb: ; = ab1bV !b#b
1107 project_root=context.project_root,
1108 include_claude=include_claude,
1109 interactive=not yes and not include_claude,
1110 )
1111 if not targets: 2bb* + , } XbT 6b7bcb- . / ~ ZbU 8b9bdb: ; = ab1bV !b#b
1112 ui.print_message("No installation targets selected.", console=console) 2XbzeAeZbBeCe1bDeEe
1113 return 2XbzeAeZbBeCe1bDeEe
1115 installed_count = _install_selected( 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b
1116 skills=selected, targets=targets, project_root=context.project_root, copy=copy
1117 )
1118 ui.print_line(console=console) 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b
1119 ui.print_summary({"installed skill targets": installed_count}, console=console) 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b
1122@app.command() 1Q23R45S67
1123def remove( 1Q23R45S67
1124 skill_names: Annotated[
1125 list[str] | None, typer.Argument(help="Names of skills to remove")
1126 ] = None,
1127 include_claude: Annotated[
1128 bool,
1129 typer.Option(
1130 "--claude",
1131 help="Also remove from .claude/skills/ alongside .agents/skills/",
1132 ),
1133 ] = False,
1134 yes: Annotated[
1135 bool, typer.Option("--yes", "-y", help="Skip interactive selection")
1136 ] = False,
1137) -> None:
1138 """Remove installed symlinked skills."""
1139 context = _get_project_context() 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1140 result = _scan_context(context) 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1141 targets = get_existing_target_dirs( 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1142 context.project_root, include_claude=include_claude
1143 )
1144 statuses = _installed_statuses(targets=targets, skills=result.skills) 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1146 selected_statuses: list[InstalledStatus] = [] 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1147 if skill_names: 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1148 selected = set(skill_names) 2qbsbIbucvctbvbJbwcxcwbybKbyczc
1149 selected_statuses = [ 2qbsbIbucvctbvbJbwcxcwbybKbyczc
1150 status
1151 for status in statuses
1152 if status.name in selected and status.type == "symlink"
1153 ]
1154 elif not yes: 2hbrb@b'd(djbub]b)d*dlbxb_b+d,d
1155 selected_statuses = _select_installed_skills_interactive(statuses) 2rb;e=eub?e@exb[e]e
1157 if not selected_statuses: 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc
1158 ui.print_message("No skills selected.", console=console) 2hb@b'd(djb]b)d*dlb_b+d,d
1159 return 2hb@b'd(djb]b)d*dlb_b+d,d
1161 for status in selected_statuses: 2qbrbsbIbucvctbubvbJbwcxcwbxbybKbyczc
1162 if uninstall_skill(status.name, status.target.path): 2qbrbsbIbucvctbubvbJbwcxcwbxbybKbyczc
1163 ui.print_result( 2qbrbsbucvctbubvbwcxcwbxbybyczc
1164 "Removed",
1165 status.name,
1166 status.target.name,
1167 _display_path(status.path, context.project_root),
1168 console=console,
1169 )
1170 else:
1171 ui.print_message( 2Ib^e_eJb`e{eKb|e}e
1172 f"Not found: [bold]{status.name}[/bold] ({status.target.name})",
1173 console=console,
1174 )
1177def main() -> None: 1Q23R45S67
1178 """CLI entry point."""
1179 app() 2~eafbfcfdfefffgfhf