feat: added remote install and install support for skills
This commit is contained in:
@@ -24,7 +24,7 @@ pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool)
|
|||||||
if layout.is_empty() {
|
if layout.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"No recognized assets found in {git_url}. Expected one or more of: \
|
"No recognized assets found in {git_url}. Expected one or more of: \
|
||||||
agents/, roles/, macros/, functions/tools/, functions/mcp.json"
|
agents/, roles/, skills/, macros/, functions/tools/, functions/mcp.json"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -193,6 +193,7 @@ fn run_git(args: Vec<OsString>) -> Result<()> {
|
|||||||
struct RemoteLayout {
|
struct RemoteLayout {
|
||||||
agents: Option<PathBuf>,
|
agents: Option<PathBuf>,
|
||||||
roles: Option<PathBuf>,
|
roles: Option<PathBuf>,
|
||||||
|
skills: Option<PathBuf>,
|
||||||
macros: Option<PathBuf>,
|
macros: Option<PathBuf>,
|
||||||
functions_tools: Option<PathBuf>,
|
functions_tools: Option<PathBuf>,
|
||||||
mcp_json: Option<PathBuf>,
|
mcp_json: Option<PathBuf>,
|
||||||
@@ -202,6 +203,7 @@ impl RemoteLayout {
|
|||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.agents.is_none()
|
self.agents.is_none()
|
||||||
&& self.roles.is_none()
|
&& self.roles.is_none()
|
||||||
|
&& self.skills.is_none()
|
||||||
&& self.macros.is_none()
|
&& self.macros.is_none()
|
||||||
&& self.functions_tools.is_none()
|
&& self.functions_tools.is_none()
|
||||||
&& self.mcp_json.is_none()
|
&& self.mcp_json.is_none()
|
||||||
@@ -215,20 +217,29 @@ fn scan_remote_layout(root: &Path) -> Result<RemoteLayout> {
|
|||||||
if agents.is_dir() {
|
if agents.is_dir() {
|
||||||
layout.agents = Some(agents);
|
layout.agents = Some(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roles = root.join("roles");
|
let roles = root.join("roles");
|
||||||
if roles.is_dir() {
|
if roles.is_dir() {
|
||||||
layout.roles = Some(roles);
|
layout.roles = Some(roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let skills = root.join("skills");
|
||||||
|
if skills.is_dir() {
|
||||||
|
layout.skills = Some(skills);
|
||||||
|
}
|
||||||
|
|
||||||
let macros = root.join("macros");
|
let macros = root.join("macros");
|
||||||
if macros.is_dir() {
|
if macros.is_dir() {
|
||||||
layout.macros = Some(macros);
|
layout.macros = Some(macros);
|
||||||
}
|
}
|
||||||
|
|
||||||
let functions = root.join("functions");
|
let functions = root.join("functions");
|
||||||
if functions.is_dir() {
|
if functions.is_dir() {
|
||||||
let tools = functions.join("tools");
|
let tools = functions.join("tools");
|
||||||
if tools.is_dir() {
|
if tools.is_dir() {
|
||||||
layout.functions_tools = Some(tools);
|
layout.functions_tools = Some(tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mcp = functions.join("mcp.json");
|
let mcp = functions.join("mcp.json");
|
||||||
if mcp.is_file() {
|
if mcp.is_file() {
|
||||||
layout.mcp_json = Some(mcp);
|
layout.mcp_json = Some(mcp);
|
||||||
@@ -251,6 +262,10 @@ fn apply_filter(mut layout: RemoteLayout, filter: Option<InstallFilter>) -> Remo
|
|||||||
roles: layout.roles.take(),
|
roles: layout.roles.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
},
|
},
|
||||||
|
InstallFilter::Skills => RemoteLayout {
|
||||||
|
skills: layout.skills.take(),
|
||||||
|
..RemoteLayout::default()
|
||||||
|
},
|
||||||
InstallFilter::Macros => RemoteLayout {
|
InstallFilter::Macros => RemoteLayout {
|
||||||
macros: layout.macros.take(),
|
macros: layout.macros.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
@@ -308,6 +323,7 @@ fn walk_files_inner(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
|
|||||||
enum TopCategory {
|
enum TopCategory {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
|
Skills,
|
||||||
Macros,
|
Macros,
|
||||||
FunctionsTools,
|
FunctionsTools,
|
||||||
}
|
}
|
||||||
@@ -317,6 +333,7 @@ impl TopCategory {
|
|||||||
match self {
|
match self {
|
||||||
TopCategory::Agents => "agents",
|
TopCategory::Agents => "agents",
|
||||||
TopCategory::Roles => "roles",
|
TopCategory::Roles => "roles",
|
||||||
|
TopCategory::Skills => "skills",
|
||||||
TopCategory::Macros => "macros",
|
TopCategory::Macros => "macros",
|
||||||
TopCategory::FunctionsTools => "functions/tools",
|
TopCategory::FunctionsTools => "functions/tools",
|
||||||
}
|
}
|
||||||
@@ -356,6 +373,11 @@ fn plan_changes(layout: &RemoteLayout) -> Result<InstallPlan> {
|
|||||||
if let Some(src_dir) = &layout.roles {
|
if let Some(src_dir) = &layout.roles {
|
||||||
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(src_dir) = &layout.skills {
|
||||||
|
plan_dir_into(src_dir, &paths::skills_dir(), TopCategory::Skills, &mut files)?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(src_dir) = &layout.macros {
|
if let Some(src_dir) = &layout.macros {
|
||||||
plan_dir_into(
|
plan_dir_into(
|
||||||
src_dir,
|
src_dir,
|
||||||
@@ -457,6 +479,7 @@ fn print_plan_summary(plan: &InstallPlan) {
|
|||||||
for cat in [
|
for cat in [
|
||||||
TopCategory::Agents,
|
TopCategory::Agents,
|
||||||
TopCategory::Roles,
|
TopCategory::Roles,
|
||||||
|
TopCategory::Skills,
|
||||||
TopCategory::Macros,
|
TopCategory::Macros,
|
||||||
TopCategory::FunctionsTools,
|
TopCategory::FunctionsTools,
|
||||||
] {
|
] {
|
||||||
@@ -982,6 +1005,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -989,8 +1013,8 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, None);
|
let out = apply_filter(l, None);
|
||||||
|
|
||||||
assert!(out.agents.is_some() && out.roles.is_some() && out.macros.is_some());
|
assert!(out.agents.is_some() && out.roles.is_some() && out.skills.is_some());
|
||||||
assert!(out.functions_tools.is_some() && out.mcp_json.is_some());
|
assert!(out.macros.is_some() && out.functions_tools.is_some() && out.mcp_json.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -998,6 +1022,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1006,6 +1031,7 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Functions));
|
let out = apply_filter(l, Some(InstallFilter::Functions));
|
||||||
|
|
||||||
assert!(out.agents.is_none());
|
assert!(out.agents.is_none());
|
||||||
|
assert!(out.skills.is_none());
|
||||||
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
||||||
assert!(out.mcp_json.is_none());
|
assert!(out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
@@ -1015,6 +1041,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1022,7 +1049,7 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
||||||
|
|
||||||
assert!(out.agents.is_none() && out.functions_tools.is_none());
|
assert!(out.agents.is_none() && out.skills.is_none() && out.functions_tools.is_none());
|
||||||
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,6 +1058,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1039,7 +1067,25 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Roles));
|
let out = apply_filter(l, Some(InstallFilter::Roles));
|
||||||
|
|
||||||
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
||||||
assert!(out.agents.is_none() && out.macros.is_none());
|
assert!(out.agents.is_none() && out.skills.is_none() && out.macros.is_none());
|
||||||
|
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_filter_skills_keeps_only_skills() {
|
||||||
|
let l = RemoteLayout {
|
||||||
|
agents: Some(PathBuf::from("a")),
|
||||||
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
|
macros: Some(PathBuf::from("m")),
|
||||||
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = apply_filter(l, Some(InstallFilter::Skills));
|
||||||
|
|
||||||
|
assert_eq!(out.skills, Some(PathBuf::from("s")));
|
||||||
|
assert!(out.agents.is_none() && out.roles.is_none() && out.macros.is_none());
|
||||||
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,8 +1130,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_finds_known_subdirs() {
|
fn scan_remote_layout_finds_known_subdirs() {
|
||||||
let root = fresh_temp_dir("scan-test-");
|
let root = fresh_temp_dir("scan-test-");
|
||||||
|
|
||||||
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
||||||
fs::create_dir_all(root.join("roles")).unwrap();
|
fs::create_dir_all(root.join("roles")).unwrap();
|
||||||
|
fs::create_dir_all(root.join("skills")).unwrap();
|
||||||
fs::create_dir_all(root.join("macros")).unwrap();
|
fs::create_dir_all(root.join("macros")).unwrap();
|
||||||
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
||||||
touch(&root.join("functions/mcp.json"));
|
touch(&root.join("functions/mcp.json"));
|
||||||
@@ -1094,12 +1142,30 @@ mod tests {
|
|||||||
let layout = scan_remote_layout(&root).unwrap();
|
let layout = scan_remote_layout(&root).unwrap();
|
||||||
assert!(layout.agents.is_some());
|
assert!(layout.agents.is_some());
|
||||||
assert!(layout.roles.is_some());
|
assert!(layout.roles.is_some());
|
||||||
|
assert!(layout.skills.is_some());
|
||||||
assert!(layout.macros.is_some());
|
assert!(layout.macros.is_some());
|
||||||
assert!(layout.functions_tools.is_some());
|
assert!(layout.functions_tools.is_some());
|
||||||
assert!(layout.mcp_json.is_some());
|
assert!(layout.mcp_json.is_some());
|
||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_remote_layout_finds_skills_only() {
|
||||||
|
let root = fresh_temp_dir("scan-skills-only-");
|
||||||
|
fs::create_dir_all(root.join("skills/git-master")).unwrap();
|
||||||
|
touch(&root.join("skills/git-master/SKILL.md"));
|
||||||
|
|
||||||
|
let layout = scan_remote_layout(&root).unwrap();
|
||||||
|
|
||||||
|
assert!(layout.skills.is_some());
|
||||||
|
assert!(layout.agents.is_none());
|
||||||
|
assert!(layout.roles.is_none());
|
||||||
|
assert!(layout.macros.is_none());
|
||||||
|
assert!(layout.functions_tools.is_none());
|
||||||
|
assert!(layout.mcp_json.is_none());
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_ignores_unrelated_files() {
|
fn scan_remote_layout_ignores_unrelated_files() {
|
||||||
let root = fresh_temp_dir("scan-unrelated-");
|
let root = fresh_temp_dir("scan-unrelated-");
|
||||||
|
|||||||
+10
-1
@@ -287,6 +287,7 @@ impl AssetCategory {
|
|||||||
pub enum InstallFilter {
|
pub enum InstallFilter {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
|
Skills,
|
||||||
Macros,
|
Macros,
|
||||||
Functions,
|
Functions,
|
||||||
#[value(name = "mcp_config")]
|
#[value(name = "mcp_config")]
|
||||||
@@ -294,12 +295,20 @@ pub enum InstallFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InstallFilter {
|
impl InstallFilter {
|
||||||
pub const NAMES: [&'static str; 5] = ["agents", "roles", "macros", "functions", "mcp_config"];
|
pub const NAMES: [&'static str; 6] = [
|
||||||
|
"agents",
|
||||||
|
"roles",
|
||||||
|
"skills",
|
||||||
|
"macros",
|
||||||
|
"functions",
|
||||||
|
"mcp_config",
|
||||||
|
];
|
||||||
|
|
||||||
pub fn parse(name: &str) -> Option<Self> {
|
pub fn parse(name: &str) -> Option<Self> {
|
||||||
match name {
|
match name {
|
||||||
"agents" => Some(Self::Agents),
|
"agents" => Some(Self::Agents),
|
||||||
"roles" => Some(Self::Roles),
|
"roles" => Some(Self::Roles),
|
||||||
|
"skills" => Some(Self::Skills),
|
||||||
"macros" => Some(Self::Macros),
|
"macros" => Some(Self::Macros),
|
||||||
"functions" => Some(Self::Functions),
|
"functions" => Some(Self::Functions),
|
||||||
"mcp_config" => Some(Self::McpConfig),
|
"mcp_config" => Some(Self::McpConfig),
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ pub fn role_file(name: &str) -> PathBuf {
|
|||||||
roles_dir().join(format!("{name}.md"))
|
roles_dir().join(format!("{name}.md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn skills_dir() -> PathBuf {
|
pub fn skills_dir() -> PathBuf {
|
||||||
match env::var(get_env_name("skills_dir")) {
|
match env::var(get_env_name("skills_dir")) {
|
||||||
Ok(value) => PathBuf::from(value),
|
Ok(value) => PathBuf::from(value),
|
||||||
|
|||||||
Reference in New Issue
Block a user