///|
let global_option : @admiral.OptionDef[Bool] = @admiral.bool(
"global",
short='g',
description="Use ~/.agents",
)
///|
let local_option : @admiral.OptionDef[String] = @admiral.string(
"local",
description="Local marketplace root beginning with ./",
)
///|
let sync_recursive_option : @admiral.OptionDef[Bool] = @admiral.bool(
"recursive",
short='r',
description="Sync from the nearest lock file and descendant lock files",
)
///|
let update_recursive_option : @admiral.OptionDef[Bool] = @admiral.bool(
"recursive",
short='r',
description="Update from the nearest lock file and descendant lock files",
)
///|
let add_repo_arg : @admiral.PositionDef[String] = @admiral.position_string(
"repo",
description="GitHub owner/repo",
required=false,
)
///|
let remove_repo_arg : @admiral.PositionDef[String] = @admiral.position_string(
"repo",
description="Repository source",
)
///|
enum SkillAddSource {
Github(String)
Local(String)
}
///|
struct SkillAddOptions {
global : Bool
source : SkillAddSource
}
///|
struct SkillGlobalOptions {
global : Bool
recursive : Bool
}
///|
struct SkillRemoveOptions {
global : Bool
source : String
}
///|
fn skill_add_command_def(
run : async (@admiral.Context) -> Unit,
) -> @admiral.CommandDef {
@admiral.command(
name="add",
description="Add a plugin marketplace",
positionals=[add_repo_arg],
options=[global_option, local_option],
run=Some(run),
)
}
///|
fn skill_sync_command_def(
run : async (@admiral.Context) -> Unit,
) -> @admiral.CommandDef {
@admiral.command(
name="sync",
description="Re-create skill symlinks",
options=[global_option, sync_recursive_option],
run=Some(run),
)
}
///|
fn skill_update_command_def(
run : async (@admiral.Context) -> Unit,
) -> @admiral.CommandDef {
@admiral.command(
name="update",
description="Update GitHub-sourced repos and sync",
options=[global_option, update_recursive_option],
run=Some(run),
)
}
///|
fn skill_remove_command_def(
run : async (@admiral.Context) -> Unit,
) -> @admiral.CommandDef {
@admiral.command(
name="remove",
description="Remove a repository from the lockfile",
positionals=[remove_repo_arg],
options=[global_option],
run=Some(run),
)
}
///|
fn skill_add_options(context : @admiral.Context) -> SkillAddOptions raise {
let global = context.get_bool(global_option)
let local_path = context.get_string(local_option)
let repo = context.get_string(add_repo_arg)
let source = match (local_path, repo) {
(Some(path), _) => {
guard path.has_prefix("./") else {
fail("--local path must start with ./")
}
Local(path)
}
(None, Some(source)) => {
validate_github_source(source)
Github(source)
}
_ => fail("Either or --local must be provided")
}
{ global, source }
}
///|
fn skill_global_options(
context : @admiral.Context,
recursive_option : @admiral.OptionDef[Bool],
) -> SkillGlobalOptions raise {
{
global: context.get_bool(global_option),
recursive: context.get_bool(recursive_option),
}
}
///|
fn skill_remove_options(context : @admiral.Context) -> SkillRemoveOptions raise {
let source = context.get_string_required(remove_repo_arg)
{ global: context.get_bool(global_option), source }
}
///|
async fn run_skill_add(context : @admiral.Context) -> Unit {
let options = skill_add_options(context)
let agents = command_agents_dir(options.global)
ensure_lock(agents)
let root = @path.Path::dirname(agents).to_string()
let (source_type, source, dir) = match options.source {
Local(path) => (RepositoryLocal, path, local_source_path(path, root))
Github(repo) => (RepositoryGithub, repo, ensure_repo(repo))
}
let market = match detect_marketplace(dir) {
Some(market) => market
None => fail("No supported marketplace found in \{dir}")
}
let (hash, lock, skills) = @async.with_task_group(group => {
let hash_task = group.spawn(() => {
match source_type {
RepositoryGithub => Some(commit_hash(dir))
RepositoryLocal => None
}
})
let lock_task = group.spawn(() => read_lock(agents))
let skills_task = group.spawn(() => resolve_skills(dir, market.kind))
(hash_task.wait(), lock_task.wait(), skills_task.wait())
})
let incoming_plugins = plugins_from_skills(skills)
let repository : LockRepository = {
source_type,
source,
marketplace_kind: market.kind,
commit_hash: hash,
plugins: incoming_plugins,
}
let updated_lock : LockFile = {
skill_dirs: lock.skill_dirs,
repositories: upsert_repository(lock.repositories, repository),
}
write_lock(agents, updated_lock)
sync_lock(agents, updated_lock)
println("Added \{source}")
}
///|
fn upsert_repository(
repositories : Array[LockRepository],
incoming : LockRepository,
) -> Array[LockRepository] {
let updated = repositories.map(fn(repo) {
if repo.source == incoming.source {
{
source_type: incoming.source_type,
source: incoming.source,
marketplace_kind: incoming.marketplace_kind,
commit_hash: incoming.commit_hash,
plugins: merge_plugins(repo.plugins, incoming.plugins),
}
} else {
repo
}
})
if repositories.any(fn(repo) { repo.source == incoming.source }) {
updated
} else {
updated.iter().concat([incoming].iter()).collect()
}
}
///|
async fn run_skill_sync(context : @admiral.Context) -> Unit {
let options = skill_global_options(context, sync_recursive_option)
let agents_dirs = if options.global {
[resolve_global_agents_dir()]
} else {
resolve_agents_dirs(options.recursive)
}
sync_agents_locks(read_agents_locks(agents_dirs))
}
///|
fn has_update_conflicts(agents_locks : Array[AgentsLock]) -> Bool {
let sources : Array[String] = agents_locks
.iter()
.flat_map(entry => {
entry.lock.repositories
.iter()
.filter_map(repo => {
match repo.source_type {
RepositoryGithub => Some(repo.source)
RepositoryLocal => None
}
})
})
.collect()
has_sync_conflicts(agents_locks) ||
unique_strings(sources).length() != sources.length()
}
///|
async fn run_skill_update(context : @admiral.Context) -> Unit {
let options = skill_global_options(context, update_recursive_option)
let agents_dirs = if options.global {
[resolve_global_agents_dir()]
} else {
resolve_agents_dirs(options.recursive)
}
let agents_locks = read_agents_locks(agents_dirs)
let tasks = agents_locks.map(entry => {
() => {
let updated_lock : LockFile = {
skill_dirs: entry.lock.skill_dirs,
repositories: update_repositories(entry.lock.repositories),
}
write_lock(entry.agents, updated_lock)
sync_lock(entry.agents, updated_lock)
}
})
if has_update_conflicts(agents_locks) {
@async.all(tasks, max_concurrent=1) |> ignore
} else {
@async.all(tasks) |> ignore
}
}
///|
async fn update_repositories(
repositories : Array[LockRepository],
) -> Array[LockRepository] {
@async.all(
repositories.map(repo => {
() => {
match repo.source_type {
RepositoryGithub => {
let dir = update_repo(repo.source)
{
source_type: repo.source_type,
source: repo.source,
marketplace_kind: repo.marketplace_kind,
commit_hash: Some(commit_hash(dir)),
plugins: repo.plugins,
}
}
RepositoryLocal => repo
}
}
}),
)
}
///|
async fn run_skill_remove(context : @admiral.Context) -> Unit {
let options = skill_remove_options(context)
let agents = command_agents_dir(options.global)
let source = options.source
let lock = read_lock(agents)
let repositories = lock.repositories
.iter()
.filter(fn(repo) { repo.source != source })
.collect()
let updated_lock : LockFile = { skill_dirs: lock.skill_dirs, repositories }
write_lock(agents, updated_lock)
sync_lock(agents, updated_lock)
println("Removed \{source}")
}