11"""agr add command implementation."""
22
3+ from pathlib import Path
4+
35from agr .commands import CommandResult
46from agr .commands ._tool_helpers import load_existing_config , save_and_summarize_results
57from agr .commands .migrations import run_tool_migrations
6- from agr .config import DEPENDENCY_TYPE_SKILL , Dependency
8+ from agr .config import DEPENDENCY_TYPE_RALPH , DEPENDENCY_TYPE_SKILL , Dependency
79from agr .console import get_console
810from agr .exceptions import (
911 INSTALL_ERROR_TYPES ,
1012 AgrError ,
13+ RalphNotFoundError ,
1114 SkillNotFoundError ,
1215 format_install_error ,
1316)
1417from agr .fetcher import (
1518 InstallResult ,
19+ fetch_and_install_ralph ,
1620 fetch_and_install_to_tools ,
1721 list_remote_repo_skills ,
1822)
2529 save_lockfile ,
2630 update_lockfile_entry ,
2731)
32+ from agr .ralph import is_valid_ralph_dir
33+ from agr .skill import is_valid_skill_dir
2834from agr .source import SourceResolver
2935
3036
37+ def _detect_local_type (source_path : Path ) -> str :
38+ """Detect whether a local path is a skill or ralph.
39+
40+ Checks for RALPH.md and SKILL.md markers. If both exist,
41+ raises an error. If neither exists, defaults to skill (existing behaviour).
42+ """
43+ has_ralph = is_valid_ralph_dir (source_path )
44+ has_skill = is_valid_skill_dir (source_path )
45+
46+ if has_ralph and has_skill :
47+ raise AgrError (
48+ f"'{ source_path } ' contains both SKILL.md and RALPH.md. "
49+ "A directory can only be one type. Remove one marker file."
50+ )
51+ if has_ralph :
52+ return DEPENDENCY_TYPE_RALPH
53+ return DEPENDENCY_TYPE_SKILL
54+
55+
3156def run_add (
3257 refs : list [str ],
3358 overwrite : bool = False ,
@@ -50,35 +75,99 @@ def run_add(
5075
5176 # Track results for summary
5277 results : list [CommandResult ] = []
53- # Track install results for lockfile
54- lockfile_updates : list [tuple [ParsedHandle , str , InstallResult ]] = []
78+ # Track install results for lockfile: (handle, ref, install_result, dep_type)
79+ lockfile_updates : list [tuple [ParsedHandle , str , InstallResult , str ]] = []
5580
5681 for ref in refs :
5782 try :
5883 # Parse handle
5984 handle = parse_handle (ref , default_owner = config .default_owner )
6085
6186 if source and handle .is_local :
62- raise AgrError ("Local skills cannot specify a source" )
87+ raise AgrError ("Local dependencies cannot specify a source" )
6388
6489 # Validate explicit source if provided
6590 if source :
6691 resolver .get (source )
6792
68- # Install the skill to all configured tools (downloads once)
69- installed_paths_dict , install_result = fetch_and_install_to_tools (
70- handle ,
71- repo_root ,
72- tools ,
73- overwrite ,
74- resolver = resolver ,
75- source = source ,
76- skills_dirs = skills_dirs ,
77- default_repo = config .default_repo ,
78- )
79- installed_paths = [
80- f"{ name } : { path } " for name , path in installed_paths_dict .items ()
81- ]
93+ # Auto-detect type and install
94+ if handle .is_local :
95+ source_path = handle .resolve_local_path (repo_root )
96+ dep_type = _detect_local_type (source_path )
97+ else :
98+ dep_type = DEPENDENCY_TYPE_SKILL # default, may change below
99+
100+ if dep_type == DEPENDENCY_TYPE_RALPH or (
101+ not handle .is_local and dep_type == DEPENDENCY_TYPE_SKILL
102+ ):
103+ # For remote handles, try skill first; if not found, try ralph
104+ if handle .is_local :
105+ # Local ralph
106+ installed_path , install_result = fetch_and_install_ralph (
107+ handle ,
108+ repo_root ,
109+ overwrite ,
110+ resolver = resolver ,
111+ source = source ,
112+ default_repo = config .default_repo ,
113+ )
114+ installed_paths = [str (installed_path )]
115+ dep_type = DEPENDENCY_TYPE_RALPH
116+ else :
117+ # Remote: try as skill first
118+ try :
119+ installed_paths_dict , install_result = (
120+ fetch_and_install_to_tools (
121+ handle ,
122+ repo_root ,
123+ tools ,
124+ overwrite ,
125+ resolver = resolver ,
126+ source = source ,
127+ skills_dirs = skills_dirs ,
128+ default_repo = config .default_repo ,
129+ )
130+ )
131+ installed_paths = [
132+ f"{ name } : { path } "
133+ for name , path in installed_paths_dict .items ()
134+ ]
135+ dep_type = DEPENDENCY_TYPE_SKILL
136+ except SkillNotFoundError :
137+ # Skill not found — try as ralph
138+ try :
139+ installed_path , install_result = fetch_and_install_ralph (
140+ handle ,
141+ repo_root ,
142+ overwrite ,
143+ resolver = resolver ,
144+ source = source ,
145+ default_repo = config .default_repo ,
146+ )
147+ installed_paths = [str (installed_path )]
148+ dep_type = DEPENDENCY_TYPE_RALPH
149+ except RalphNotFoundError :
150+ # Neither skill nor ralph found — re-raise as skill error
151+ # for backwards-compatible error messages
152+ raise SkillNotFoundError (
153+ f"'{ handle .name } ' not found as a skill or ralph "
154+ f"in any configured source."
155+ ) from None
156+ else :
157+ # Local skill (already detected)
158+ installed_paths_dict , install_result = fetch_and_install_to_tools (
159+ handle ,
160+ repo_root ,
161+ tools ,
162+ overwrite ,
163+ resolver = resolver ,
164+ source = source ,
165+ skills_dirs = skills_dirs ,
166+ default_repo = config .default_repo ,
167+ )
168+ installed_paths = [
169+ f"{ name } : { path } " for name , path in installed_paths_dict .items ()
170+ ]
82171
83172 # Add to config
84173 if handle .is_local :
@@ -87,21 +176,21 @@ def run_add(
87176 path_value = str (handle .resolve_local_path ())
88177 config .add_dependency (
89178 Dependency (
90- type = DEPENDENCY_TYPE_SKILL ,
179+ type = dep_type ,
91180 path = path_value ,
92181 )
93182 )
94183 else :
95184 config .add_dependency (
96185 Dependency (
97- type = DEPENDENCY_TYPE_SKILL ,
186+ type = dep_type ,
98187 handle = handle .to_toml_handle (),
99188 source = source ,
100189 ),
101190 also_matches = [ref ],
102191 )
103192
104- lockfile_updates .append ((handle , ref , install_result ))
193+ lockfile_updates .append ((handle , ref , install_result , dep_type ))
105194 results .append (CommandResult (ref , True , ", " .join (installed_paths )))
106195
107196 except SkillNotFoundError as e :
@@ -131,11 +220,13 @@ def _print_add_result(result: CommandResult) -> None:
131220 if lockfile_updates :
132221 lockfile_path = build_lockfile_path (config_path )
133222 lockfile = load_lockfile (lockfile_path ) or Lockfile ()
134- for handle , ref , install_result in lockfile_updates :
223+ for handle , ref , install_result , dep_type in lockfile_updates :
224+ is_ralph = dep_type == DEPENDENCY_TYPE_RALPH
135225 if handle .is_local :
136226 update_lockfile_entry (
137227 lockfile ,
138228 LockedSkill (path = ref , installed_name = handle .name ),
229+ ralph = is_ralph ,
139230 )
140231 else :
141232 update_lockfile_entry (
@@ -147,6 +238,7 @@ def _print_add_result(result: CommandResult) -> None:
147238 content_hash = install_result .content_hash ,
148239 installed_name = handle .name ,
149240 ),
241+ ralph = is_ralph ,
150242 )
151243 save_lockfile (lockfile , lockfile_path )
152244
0 commit comments