///|
let lock_plugin_name_lens : @lens.PresenceLens[Json] = @lens.root()
.json("name")
.optional()
///|
let lock_plugin_path_lens : @lens.PresenceLens[Json] = @lens.root()
.json("path")
.optional()
///|
let lock_plugin_enabled_skills_lens : @lens.PresenceLens[Json] = @lens.root()
.json("enabledSkills")
.optional()
///|
let lock_repository_source_lens : @lens.PresenceLens[Json] = @lens.root()
.json("source")
.optional()
///|
let lock_repository_source_type_lens : @lens.PresenceLens[Json] = @lens.root()
.json("sourceType")
.optional()
///|
let lock_repository_marketplace_kind_lens : @lens.PresenceLens[Json] = @lens.root()
.json("marketplaceKind")
.optional()
///|
let lock_repository_commit_hash_lens : @lens.PresenceLens[Json] = @lens.root()
.json("commitHash")
.optional()
///|
let lock_repository_plugins_lens : @lens.PresenceLens[Json] = @lens.root()
.json("plugins")
.optional()
///|
let lock_file_version_lens : @lens.PresenceLens[Json] = @lens.root()
.json("version")
.optional()
///|
let lock_file_skill_dirs_lens : @lens.PresenceLens[Json] = @lens.root()
.json("skillDirs")
.optional()
///|
let lock_file_repositories_lens : @lens.PresenceLens[Json] = @lens.root()
.json("repositories")
.optional()
///|
struct LockPlugin {
name : String
path : String
enabled_skills : Array[String]
}
///|
struct LockRepository {
source_type : RepositorySourceType
source : String
marketplace_kind : MarketplaceKind
commit_hash : String?
plugins : Array[LockPlugin]
}
///|
enum RepositorySourceType {
RepositoryLocal
RepositoryGithub
}
///|
impl FromJson for RepositorySourceType with fn from_json(json, path) {
let value : String = @json.from_json(json, path~)
match value {
"local" => RepositoryLocal
"github" => RepositoryGithub
_ =>
raise @json.JsonDecodeError(
(path, "unsupported repository source type: \{value}"),
)
}
}
///|
impl FromJson for MarketplaceKind with fn from_json(json, path) {
let value : String = @json.from_json(json, path~)
match value {
"claude" => Claude
"cursor" => Cursor
"codex" => Codex
_ =>
raise @json.JsonDecodeError(
(path, "unsupported marketplace kind: \{value}"),
)
}
}
///|
impl FromJson for LockPlugin with fn from_json(json, path) {
guard json is Json::Object(_) else {
raise @json.JsonDecodeError((path, "expected lock plugin object"))
}
let name_path = lock_plugin_name_lens.add_to_json_path(path)
let plugin_path = lock_plugin_path_lens.add_to_json_path(path)
let enabled_skills_path = lock_plugin_enabled_skills_lens.add_to_json_path(
path,
)
let name_value = lock_plugin_name_lens.get(json) catch { _ => None }
let name = match name_value {
Some(value) =>
@json.from_json(value, path=name_path) catch {
_ => "plugin"
}
None => "plugin"
}
let path_value = lock_plugin_path_lens.get(json) catch { _ => None }
let path = match path_value {
Some(value) => @json.from_json(value, path=plugin_path) catch { _ => name }
None => name
}
let enabled_skills_value = lock_plugin_enabled_skills_lens.get(json) catch {
_ => None
}
let enabled_skills = match enabled_skills_value {
Some(Json::Array(values)) =>
values
.iter()
.filter_map(fn(value) {
Some((@json.from_json(value, path=enabled_skills_path) : String)) catch {
_ => None
}
})
.collect()
_ => []
}
{ name, path, enabled_skills }
}
///|
impl FromJson for LockRepository with fn from_json(json, path) {
guard json is Json::Object(_) else {
raise @json.JsonDecodeError((path, "expected lock repository object"))
}
let source_path = lock_repository_source_lens.add_to_json_path(path)
let source_type_path = lock_repository_source_type_lens.add_to_json_path(path)
let marketplace_kind_path = lock_repository_marketplace_kind_lens.add_to_json_path(
path,
)
let commit_hash_path = lock_repository_commit_hash_lens.add_to_json_path(path)
let plugins_path = lock_repository_plugins_lens.add_to_json_path(path)
let source_value = lock_repository_source_lens.get(json) catch { _ => None }
let source = match source_value {
Some(value) => @json.from_json(value, path=source_path) catch { _ => "" }
None => ""
}
guard source != "" else {
raise lock_repository_source_lens.json_decode_error(
path, "lock repository source is required",
)
}
let source_type_value = lock_repository_source_type_lens.get(json) catch {
_ => None
}
let source_type = match source_type_value {
Some(value) =>
match value {
Json::String(_) => @json.from_json(value, path=source_type_path)
_ => RepositoryLocal
}
None => RepositoryLocal
}
let marketplace_kind_value = lock_repository_marketplace_kind_lens.get(json) catch {
_ => None
}
let marketplace_kind = match marketplace_kind_value {
Some(value) =>
match value {
Json::String(_) => @json.from_json(value, path=marketplace_kind_path)
_ => Claude
}
None => Claude
}
let commit_hash_value = lock_repository_commit_hash_lens.get(json) catch {
_ => None
}
let commit_hash = match commit_hash_value {
Some(value) =>
Some((@json.from_json(value, path=commit_hash_path) : String)) catch {
_ => None
}
None => None
}
let plugins_value = lock_repository_plugins_lens.get(json) catch { _ => None }
let plugins = match plugins_value {
Some(Json::Array(values)) =>
values
.iter()
.filter_map(fn(value) {
Some((@json.from_json(value, path=plugins_path) : LockPlugin)) catch {
_ => None
}
})
.collect()
_ => []
}
{ source_type, source, marketplace_kind, commit_hash, plugins }
}
///|
impl ToJson for LockPlugin with fn to_json(self) -> Json {
let builder = @lens.JsonBuilder::JsonBuilder()
lock_plugin_name_lens.set_or_abort(builder, Some(self.name.to_json()))
lock_plugin_path_lens.set_or_abort(builder, Some(self.path.to_json()))
lock_plugin_enabled_skills_lens.set_or_abort(
builder,
Some(self.enabled_skills.to_json()),
)
builder.to_json()
}
///|
impl ToJson for LockRepository with fn to_json(self) -> Json {
let builder = @lens.JsonBuilder::JsonBuilder()
lock_repository_source_type_lens.set_or_abort(
builder,
Some(repository_source_type_to_string(self.source_type).to_json()),
)
lock_repository_source_lens.set_or_abort(builder, Some(self.source.to_json()))
lock_repository_commit_hash_lens.set_or_abort(
builder,
self.commit_hash.map(fn(value) { value.to_json() }),
)
lock_repository_marketplace_kind_lens.set_or_abort(
builder,
Some(marketplace_kind_to_string(self.marketplace_kind).to_json()),
)
lock_repository_plugins_lens.set_or_abort(
builder,
Some(self.plugins.to_json()),
)
builder.to_json()
}
///|
impl ToJson for LockFile with fn to_json(self) -> Json {
let builder = @lens.JsonBuilder::JsonBuilder()
lock_file_version_lens.set_or_abort(
builder,
Some(Json::number(1.0, repr="1")),
)
lock_file_skill_dirs_lens.set_or_abort(
builder,
Some(self.skill_dirs.to_json()),
)
lock_file_repositories_lens.set_or_abort(
builder,
Some(self.repositories.to_json()),
)
builder.to_json()
}
///|
extend LockFile with ToJson::{to_json}
///|
fn repository_source_type_to_string(
source_type : RepositorySourceType,
) -> String {
match source_type {
RepositoryLocal => "local"
RepositoryGithub => "github"
}
}
///|
fn empty_lock_file() -> LockFile {
{ skill_dirs: [], repositories: [] }
}
///|
struct LockFile {
skill_dirs : Array[String]
repositories : Array[LockRepository]
}
///|
impl FromJson for LockFile with fn from_json(json, path) {
guard json is Json::Object(_) else {
raise @json.JsonDecodeError((path, "expected lock file object"))
}
let skill_dirs_path = lock_file_skill_dirs_lens.add_to_json_path(path)
let repositories_path = lock_file_repositories_lens.add_to_json_path(path)
let version_value = lock_file_version_lens.get(json) catch { _ => None }
let version = match version_value {
Some(Json::Number(value, ..)) => value
_ => 0.0
}
guard version == 1.0 else {
raise lock_file_version_lens.json_decode_error(
path, "unsupported lock file version",
)
}
let skill_dirs_value = lock_file_skill_dirs_lens.get(json) catch { _ => None }
let skill_dirs = match skill_dirs_value {
Some(Json::Array(values)) =>
values
.iter()
.filter_map(fn(value) {
Some((@json.from_json(value, path=skill_dirs_path) : String)) catch {
_ => None
}
})
.collect()
_ => []
}
let repositories_value = lock_file_repositories_lens.get(json) catch {
_ => None
}
let repositories = match repositories_value {
Some(Json::Array(values)) =>
values
.iter()
.filter_map(fn(value) {
Some((@json.from_json(value, path=repositories_path) : LockRepository)) catch {
_ => None
}
})
.collect()
_ => []
}
{ skill_dirs, repositories }
}
///|
fn empty_lock_text() -> String {
empty_lock_file().to_json().stringify(indent=2) + "\n"
}
///|
async fn ensure_lock(agents : String) -> Unit {
if !@fs.exists(agents) {
@fs.mkdir(agents, recursive=true)
}
let path = lock_path_from_agents(agents)
if !@fs.exists(path) {
@fs.write_file(path, empty_lock_text(), create_mode=CreateOrTruncate)
}
}
///|
async fn read_lock(agents : String) -> LockFile {
let path = lock_path_from_agents(agents)
let text = if @fs.exists(path) {
@fs.read_file(path).text()
} else {
empty_lock_text()
}
let document = @json.parse(text) catch { _ => return empty_lock_file() }
(@json.from_json(document) : LockFile) catch {
_ => empty_lock_file()
}
}
///|
async fn write_lock(agents : String, lock : LockFile) -> Unit {
@fs.write_file(
lock_path_from_agents(agents),
lock.to_json().stringify(indent=2) + "\n",
create_mode=CreateOrTruncate,
)
}