///|
async fn ensure_repo(source : String) -> String {
validate_github_source(source)
let dir = repo_cache_dir(source)
if @fs.exists(@path.Path::join(dir, ".git").to_string()) {
run_checked("git", ["-C", dir, "fetch", "--depth", "1", "origin"])
} else {
@fs.mkdir(@path.Path::dirname(dir).to_string(), recursive=true)
run_checked("git", [
"clone",
"--depth",
"1",
"https://github.com/\{source}.git",
dir,
])
}
dir
}
///|
async fn update_repo(source : String) -> String {
let dir = ensure_repo(source)
let default_ref = collect_or_empty("git", [
"-C", dir, "symbolic-ref", "--short", "refs/remotes/origin/HEAD",
])
let branch = if default_ref.has_prefix("origin/") {
default_ref["origin/".length():].to_owned()
} else {
"main"
}
run_checked("git", ["-C", dir, "checkout", branch])
run_checked("git", ["-C", dir, "pull", "--ff-only", "origin", branch])
dir
}
///|
async fn repo_dir_for(lock_path : String, repo : LockRepository) -> String {
match repo.source_type {
RepositoryLocal =>
local_source_path(repo.source, @path.Path::dirname(lock_path).to_string())
RepositoryGithub => {
let dir = ensure_repo(repo.source)
match repo.commit_hash {
Some(hash) => run_checked("git", ["-C", dir, "checkout", hash])
None => ()
}
dir
}
}
}
///|
async fn commit_hash(repo_dir : String) -> String {
collect_or_empty("git", ["-C", repo_dir, "rev-parse", "HEAD"])
}
///|
struct AgentsLock {
agents : String
lock : LockFile
}
///|
fn unique_strings(values : Array[String]) -> Array[String] {
values
.iter()
.fold(init=[], fn(unique, value) {
if unique.contains(value) {
unique
} else {
unique.iter().concat([value].iter()).collect()
}
})
}
///|
fn managed_skill_roots(
agents : String,
skill_dirs : Array[String],
) -> Array[String] {
[@path.Path::join(agents, "skills").to_string()]
.iter()
.concat(skill_dirs.iter().map(expand_home))
.collect()
|> unique_strings
}
///|
fn has_sync_conflicts(agents_locks : Array[AgentsLock]) -> Bool {
let roots : Array[String] = agents_locks
.iter()
.flat_map(entry => {
managed_skill_roots(entry.agents, entry.lock.skill_dirs).iter()
})
.collect()
unique_strings(roots).length() != roots.length()
}
///|
async fn read_agents_locks(agents_dirs : Array[String]) -> Array[AgentsLock] {
@async.all(
agents_dirs.map(agents => () => { agents, lock: read_lock(agents) }),
)
}
///|
async fn create_skill_link_in(
root : String,
skill_name : String,
skill_path : String,
) -> Unit {
if !@fs.exists(root) {
@fs.mkdir(root, recursive=true)
}
let link = @path.Path::join(root, skill_name).to_string()
let kind = @fs.kind(link, follow_symlink=false) catch {
_ => @fs.FileKind::Unknown
}
if kind == @fs.FileKind::SymLink {
@fs.remove(link) catch {
_ => ()
}
} else if @fs.exists(link) {
fail("Refusing to replace non-symlink path: \{link}")
}
@fs.symlink(target=relative_path(root, skill_path), link)
}
///|
async fn create_skill_link(
agents : String,
skill_dirs : Array[String],
skill_name : String,
skill_path : String,
) -> Unit {
let target_roots = managed_skill_roots(agents, skill_dirs)
@async.all(
target_roots.map(root => {
() => create_skill_link_in(root, skill_name, skill_path)
}),
)
|> ignore
}
///|
async fn clear_skill_links(root : String) -> Unit {
let entries = @fs.readdir(
root,
include_hidden=false,
include_special=false,
sort=true,
) catch {
_ => []
}
@async.all(
entries.map(entry => {
() => {
let path = @path.Path::join(root, entry).to_string()
let kind = @fs.kind(path, follow_symlink=false) catch {
_ => @fs.FileKind::Unknown
}
if kind == @fs.FileKind::SymLink {
@fs.remove(path) catch {
_ => ()
}
}
}
}),
)
|> ignore
}
///|
async fn clear_managed_links(
agents : String,
skill_dirs : Array[String],
) -> Unit {
let roots = managed_skill_roots(agents, skill_dirs)
@async.all(roots.map(root => () => clear_skill_links(root))) |> ignore
}
///|
async fn resolve_enabled_skills(
agents : String,
repo : LockRepository,
) -> Array[ResolvedSkill] {
let repo_dir = repo_dir_for(lock_path_from_agents(agents), repo)
let skills = resolve_skills(repo_dir, repo.marketplace_kind)
let expected_skills : Array[String] = repo.plugins
.iter()
.flat_map(fn(plugin) { plugin.enabled_skills.iter() })
.collect()
skills
.iter()
.filter(skill => expected_skills.contains(skill.skill_name))
.collect()
}
///|
fn deduplicate_resolved_skills(
skills : Array[ResolvedSkill],
) -> Array[ResolvedSkill] {
skills
.iter()
.fold(init=[], fn(unique, incoming) {
unique
.iter()
.filter(skill => skill.skill_name != incoming.skill_name)
.concat([incoming].iter())
.collect()
})
}
///|
async fn sync_lock(agents : String, lock : LockFile) -> Unit {
clear_managed_links(agents, lock.skill_dirs)
let skills = @async.all(
lock.repositories.map(repo => {
() => {
resolve_enabled_skills(agents, repo) catch {
err if @async.is_being_cancelled() => raise err
err => {
println("Skipped \{repo.source}: \{err}")
[]
}
}
}
}),
)
.iter()
.flat_map(resolved => resolved.iter())
.collect()
|> deduplicate_resolved_skills
@async.all(
skills.map(skill => {
() => {
create_skill_link(
agents,
lock.skill_dirs,
skill.skill_name,
skill.skill_path,
)
}
}),
)
|> ignore
}
///|
async fn sync_agents_locks(agents_locks : Array[AgentsLock]) -> Unit {
let tasks = agents_locks.map(entry => {
() => {
sync_lock(entry.agents, entry.lock)
write_lock(entry.agents, entry.lock)
}
})
if has_sync_conflicts(agents_locks) {
@async.all(tasks, max_concurrent=1) |> ignore
} else {
@async.all(tasks) |> ignore
}
}