add epm — El Package Manager
Introduces epm/, a new component written entirely in native El.
epm manages vessels (El's deployable package format): publish to Engram,
install with full dependency resolution, list registry contents, and
inspect vessel metadata.
- epm/manifest.el — package manifest
- epm/src/manifest.el — vessel/package manifest parser (line-by-line,
same approach as elb.el)
- epm/src/registry.el — Engram-backed vessel registry (POST /api/nodes,
GET /api/search); vessels stored as Entity nodes
with label "vessel:<name>:<version>"
- epm/src/install.el — topological dependency resolver with cycle
detection; installs to .epm/vessels/<name>/
- epm/src/epm.el — main entry point: publish / install / list / info
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
// epm — El Package Manager
|
||||
//
|
||||
// Manages vessels: publish, install, resolve dependencies.
|
||||
// Vessels are stored in Engram as nodes. epm reads the local manifest.el,
|
||||
// talks to Engram over HTTP, and writes installed vessels to .epm/vessels/.
|
||||
|
||||
package "epm" {
|
||||
version "0.1.0"
|
||||
description "El Package Manager — vessel registry, publish, install, resolve"
|
||||
authors ["Will Anderson <will@neurontechnologies.ai>"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/epm.el"
|
||||
output "dist/"
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// epm/src/epm.el — El Package Manager entry point
|
||||
//
|
||||
// epm manages vessels: the deployable, versioned units of El code.
|
||||
// Vessels are stored in Engram (the graph database) and installed locally
|
||||
// under .epm/vessels/.
|
||||
//
|
||||
// Usage:
|
||||
// epm publish # publish current project to registry
|
||||
// epm install # install all dependencies from manifest.el
|
||||
// epm install <vessel> # install a specific vessel
|
||||
// epm list # list all vessels in the registry
|
||||
// epm info <vessel> # show metadata for a vessel
|
||||
// epm info <vessel> <version> # show metadata for a specific version
|
||||
//
|
||||
// Configuration (environment):
|
||||
// ENGRAM_URL — Engram base URL (default: http://localhost:8742)
|
||||
//
|
||||
// Import order: manifest.el, registry.el, install.el must be compiled first.
|
||||
// This file imports all three and is the build entry point.
|
||||
|
||||
import "manifest.el"
|
||||
import "registry.el"
|
||||
import "install.el"
|
||||
|
||||
// ── Subcommand handlers ───────────────────────────────────────────────────────
|
||||
|
||||
// cmd_publish reads manifest.el from the current directory and publishes
|
||||
// the vessel/package to the Engram registry.
|
||||
fn cmd_publish() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = manifest_name(src)
|
||||
if str_eq(name, "") {
|
||||
println("epm: manifest.el has no 'vessel' or 'package' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let version: String = manifest_version(src)
|
||||
if str_eq(version, "") {
|
||||
println("epm: manifest.el has no 'version' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let description: String = manifest_description(src)
|
||||
let entry: String = manifest_entry(src)
|
||||
let deps_json: String = manifest_deps(src)
|
||||
|
||||
println("epm: publishing " + name + " " + version + " ...")
|
||||
|
||||
let node_id: String = registry_publish(name, version, description, entry, deps_json)
|
||||
if str_eq(node_id, "") {
|
||||
println("epm: publish failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("epm: published " + name + " " + version + " (node: " + node_id + ")")
|
||||
}
|
||||
|
||||
// cmd_install_named installs a specific vessel by name (with optional version).
|
||||
// Expects: install <name> or install <name>@<version>
|
||||
fn cmd_install_named(spec: String) -> Void {
|
||||
// spec may be "el-auth" or "el-auth@0.1.0"
|
||||
let at: Int = str_index_of(spec, "@")
|
||||
let vname: String = spec
|
||||
let vver: String = ""
|
||||
if at > 0 {
|
||||
let vname = str_slice(spec, 0, at)
|
||||
let vver = str_slice(spec, at + 1, str_len(spec))
|
||||
}
|
||||
|
||||
println("epm: installing " + vname + " ...")
|
||||
|
||||
let mk1: Int = exec_command("mkdir -p .epm/vessels")
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_list prints all vessels registered in Engram.
|
||||
fn cmd_list() -> Void {
|
||||
println("epm: fetching vessel list from registry ...")
|
||||
let raw: String = registry_list()
|
||||
let n: Int = json_array_len(raw)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: no vessels found in registry")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: " + native_int_to_str(n) + " vessel(s) in registry:")
|
||||
println("")
|
||||
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
// Each element is a quoted+escaped content JSON blob
|
||||
let elem: String = json_array_get_string(raw, i)
|
||||
let vname: String = json_get_string(elem, "name")
|
||||
let vver: String = json_get_string(elem, "version")
|
||||
let vdesc: String = json_get_string(elem, "description")
|
||||
println(" " + vname + " " + vver + " — " + vdesc)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_info prints detailed metadata for a specific vessel.
|
||||
// argv is the full args list; idx is the position of the vessel name.
|
||||
fn cmd_info(argv: [String], idx: Int) -> Void {
|
||||
let argc: Int = native_list_len(argv)
|
||||
if idx >= argc {
|
||||
println("epm: usage: epm info <vessel> [version]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let vname: String = native_list_get(argv, idx)
|
||||
let vver: String = ""
|
||||
if idx + 1 < argc {
|
||||
let vver = native_list_get(argv, idx + 1)
|
||||
}
|
||||
|
||||
let content: String = registry_find(vname, vver)
|
||||
if str_eq(content, "") {
|
||||
println("epm: vessel '" + vname + "' not found in registry")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let name: String = json_get_string(content, "name")
|
||||
let ver: String = json_get_string(content, "version")
|
||||
let desc: String = json_get_string(content, "description")
|
||||
let entry: String = json_get_string(content, "entry")
|
||||
let deps_raw: String = json_get_raw(content, "deps")
|
||||
|
||||
println("name: " + name)
|
||||
println("version: " + ver)
|
||||
println("description: " + desc)
|
||||
println("entry: " + entry)
|
||||
|
||||
let dep_n: Int = json_array_len(deps_raw)
|
||||
if dep_n == 0 {
|
||||
println("dependencies: (none)")
|
||||
} else {
|
||||
println("dependencies:")
|
||||
let di: Int = 0
|
||||
while di < dep_n {
|
||||
let dep: String = json_array_get(deps_raw, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
println(" " + dep_name + " " + dep_ver)
|
||||
let di = di + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
let argc: Int = native_list_len(argv)
|
||||
|
||||
// argv[0] is the program name; subcommand is argv[1]
|
||||
if argc < 2 {
|
||||
println("epm — El Package Manager")
|
||||
println("")
|
||||
println("usage:")
|
||||
println(" epm publish publish current project to registry")
|
||||
println(" epm install install all dependencies (from manifest.el)")
|
||||
println(" epm install <vessel> install a specific vessel")
|
||||
println(" epm install <vessel>@<ver> install a specific vessel at a version")
|
||||
println(" epm list list all vessels in the registry")
|
||||
println(" epm info <vessel> show vessel metadata")
|
||||
println(" epm info <vessel> <version> show metadata for a specific version")
|
||||
println("")
|
||||
println("configuration:")
|
||||
println(" ENGRAM_URL Engram base URL (default: http://localhost:8742)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let sub: String = native_list_get(argv, 1)
|
||||
|
||||
if str_eq(sub, "publish") {
|
||||
cmd_publish()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "install") {
|
||||
if argc >= 3 {
|
||||
let spec: String = native_list_get(argv, 2)
|
||||
cmd_install_named(spec)
|
||||
} else {
|
||||
cmd_install()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "list") {
|
||||
cmd_list()
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "info") {
|
||||
cmd_info(argv, 2)
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: unknown subcommand '" + sub + "'")
|
||||
println("run 'epm' with no arguments for usage")
|
||||
exit(1)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// epm/src/install.el — dependency resolution and vessel installation
|
||||
//
|
||||
// Reads the current project's manifest.el, walks the full dependency graph
|
||||
// by fetching each dep from Engram, detects cycles, and produces a
|
||||
// topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
//
|
||||
// Cycle detection: each recursion carries a chain of names currently on
|
||||
// the call stack. If a dep already appears in the chain it's a cycle.
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Parse manifest.el to get direct deps
|
||||
// 2. For each dep, fetch its manifest from Engram
|
||||
// 3. Recurse into that dep's own deps (depth-first)
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── List helpers (operating on String lists) ──────────────────────────────────
|
||||
|
||||
// list_contains returns true if item appears in lst.
|
||||
fn list_contains(lst: [String], item: String) -> Bool {
|
||||
let n: Int = native_list_len(lst)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: String = native_list_get(lst, i)
|
||||
if str_eq(v, item) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name to resolve
|
||||
// version — version string (may be "" for any)
|
||||
// chain — names currently on the recursion stack (cycle detection)
|
||||
// visited — names already appended to order (dedup)
|
||||
// order — accumulated result (list of "name:version" strings)
|
||||
//
|
||||
// Returns a Map with keys "visited", "order", and "ok" (Bool as String).
|
||||
fn resolve_deps_recursive(name: String, version: String, chain: [String], visited: [String], order: [String]) -> Map<String, Any> {
|
||||
let key: String = name + ":" + version
|
||||
|
||||
// Skip if already ordered
|
||||
if list_contains(visited, key) {
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// Cycle detection: if name is already in the call chain, it's a cycle
|
||||
if list_contains(chain, name) {
|
||||
println("epm: error: dependency cycle detected involving '" + name + "'")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// content is a JSON blob: {"name":..., "version":..., "deps":[...]}
|
||||
let actual_version: String = json_get_string(content, "version")
|
||||
let actual_key: String = name + ":" + actual_version
|
||||
let deps_json: String = json_get_raw(content, "deps")
|
||||
|
||||
// Walk this vessel's dependencies first (depth-first)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
let di: Int = 0
|
||||
while di < dep_count {
|
||||
let dep: String = json_array_get(deps_json, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return r
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let di = di + 1
|
||||
}
|
||||
|
||||
// Now append this vessel (after all its deps)
|
||||
let visited = native_list_append(visited, actual_key)
|
||||
let order = native_list_append(order, actual_key)
|
||||
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// ── Install order ─────────────────────────────────────────────────────────────
|
||||
|
||||
// resolve_install_order reads manifest.el from the current directory and
|
||||
// returns a list of "name:version" strings in topological install order.
|
||||
//
|
||||
// Returns an empty list on failure (error is printed to stdout).
|
||||
fn resolve_install_order() -> [String] {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let deps_json: String = manifest_deps(src)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
if dep_count == 0 {
|
||||
println("epm: no dependencies declared in manifest.el")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let chain: [String] = native_list_empty()
|
||||
let visited: [String] = native_list_empty()
|
||||
let order: [String] = native_list_empty()
|
||||
|
||||
let i: Int = 0
|
||||
while i < dep_count {
|
||||
let dep: String = json_array_get(deps_json, i)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return native_list_empty()
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// ── Installation ──────────────────────────────────────────────────────────────
|
||||
|
||||
// install_vessel fetches a vessel's source from Engram and writes it
|
||||
// to .epm/vessels/<name>/.
|
||||
//
|
||||
// Currently writes the content JSON blob as a manifest record so that
|
||||
// other tools can introspect what is installed. When Engram stores actual
|
||||
// source archives, this function would extract them.
|
||||
//
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)
|
||||
|
||||
// Write the content blob as installed.json so downstream tools know what's there
|
||||
let dest: String = install_dir + "/installed.json"
|
||||
fs_write(dest, content)
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Top-level install ─────────────────────────────────────────────────────────
|
||||
|
||||
// cmd_install resolves and installs all dependencies for the current project.
|
||||
fn cmd_install() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let pkg_name: String = manifest_name(src)
|
||||
println("epm: resolving dependencies for " + pkg_name)
|
||||
|
||||
let order: [String] = resolve_install_order()
|
||||
let n: Int = native_list_len(order)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: nothing to install")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: install order (" + native_int_to_str(n) + " vessels):")
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(order, i)
|
||||
println(" " + entry)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
|
||||
// Install each vessel in topological order
|
||||
let j: Int = 0
|
||||
while j < n {
|
||||
let kv: String = native_list_get(order, j)
|
||||
// Split "name:version" on the last colon
|
||||
let colon: Int = str_index_of(kv, ":")
|
||||
if colon < 0 {
|
||||
println("epm: error: malformed order entry: " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let vname: String = str_slice(kv, 0, colon)
|
||||
let vver: String = str_slice(kv, colon + 1, str_len(kv))
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed at " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
println("epm: all dependencies installed")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// epm/src/manifest.el — vessel and package manifest parser
|
||||
//
|
||||
// Parses manifest.el files in the vessel/package format:
|
||||
//
|
||||
// vessel "el-auth" {
|
||||
// version "0.1.0"
|
||||
// description "..."
|
||||
// authors ["..."]
|
||||
// edition "2026"
|
||||
// }
|
||||
//
|
||||
// dependencies {
|
||||
// el-platform "1.0"
|
||||
// el-identity "0.1"
|
||||
// }
|
||||
//
|
||||
// build {
|
||||
// entry "src/main.el"
|
||||
// output "dist/"
|
||||
// }
|
||||
//
|
||||
// Parsing is line-by-line string extraction, same approach as elb.el.
|
||||
// No AST, no grammar — sufficient for well-formed manifests.
|
||||
|
||||
// ── String extraction helpers ─────────────────────────────────────────────────
|
||||
|
||||
// extract_quoted extracts the first double-quoted token from a string.
|
||||
// Returns "" if no quoted token is found.
|
||||
// Example: extract_quoted("vessel \"el-auth\" {") -> "el-auth"
|
||||
fn extract_quoted(s: String) -> String {
|
||||
let first: Int = str_index_of(s, "\"")
|
||||
if first < 0 { return "" }
|
||||
let after: String = str_slice(s, first + 1, str_len(s))
|
||||
let close: Int = str_index_of(after, "\"")
|
||||
if close < 0 { return "" }
|
||||
return str_slice(after, 0, close)
|
||||
}
|
||||
|
||||
// ── Manifest field extraction ─────────────────────────────────────────────────
|
||||
|
||||
// manifest_name extracts the vessel or package name from manifest source.
|
||||
// Recognises both "vessel" and "package" block headers.
|
||||
fn manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
if str_starts_with(t, "package ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_version extracts the version string from manifest source.
|
||||
fn manifest_version(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "version ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_description extracts the description string from manifest source.
|
||||
fn manifest_description(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "description ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_entry extracts the build entry file from manifest source.
|
||||
fn manifest_entry(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "entry ") {
|
||||
return extract_quoted(t)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// manifest_deps extracts dependencies as a JSON array of {"name":..., "version":...} objects.
|
||||
//
|
||||
// Scans lines inside the dependencies { ... } block.
|
||||
// Each dep line looks like:
|
||||
// el-platform "1.0"
|
||||
//
|
||||
// Returns a JSON array string, e.g.:
|
||||
// [{"name":"el-platform","version":"1.0"},{"name":"el-identity","version":"0.1"}]
|
||||
fn manifest_deps(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let in_deps: Bool = false
|
||||
let result: String = "["
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
|
||||
// Detect entry into dependencies block
|
||||
if str_eq(t, "dependencies {") {
|
||||
let in_deps = true
|
||||
let i = i + 1
|
||||
} else {
|
||||
if in_deps {
|
||||
// Detect exit from block
|
||||
if str_eq(t, "}") {
|
||||
let in_deps = false
|
||||
} else {
|
||||
// Each dep line: name "version"
|
||||
// Find first space to split name from quoted version
|
||||
let sp: Int = str_index_of(t, " ")
|
||||
if sp > 0 {
|
||||
let dep_name: String = str_slice(t, 0, sp)
|
||||
let rest: String = str_trim(str_slice(t, sp, str_len(t)))
|
||||
let dep_ver: String = extract_quoted(rest)
|
||||
if !str_eq(dep_name, "") {
|
||||
let entry: String = "{\"name\":\"" + dep_name + "\",\"version\":\"" + dep_ver + "\"}"
|
||||
if count == 0 {
|
||||
let result = result + entry
|
||||
} else {
|
||||
let result = result + "," + entry
|
||||
}
|
||||
let count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// manifest_is_vessel returns true if the manifest declares a vessel (not a package).
|
||||
fn manifest_is_vessel(src: String) -> Bool {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "vessel ") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// epm/src/registry.el — Engram-backed vessel registry
|
||||
//
|
||||
// Vessels are stored in Engram as nodes with a structured label scheme:
|
||||
//
|
||||
// label: "vessel:<name>:<version>" (used as the search key)
|
||||
// content: JSON blob with full vessel metadata
|
||||
// node_type: "Entity"
|
||||
// salience: 0.9 (vessels are high-salience, long-lived knowledge)
|
||||
//
|
||||
// All registry operations go over HTTP to Engram. The Engram URL is read
|
||||
// from the ENGRAM_URL environment variable; defaults to http://localhost:8742.
|
||||
//
|
||||
// Endpoints used:
|
||||
// POST /api/nodes — publish a vessel node
|
||||
// GET /api/search?q=... — find vessels by label prefix
|
||||
|
||||
// ── Engram URL ────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_url returns the base URL for Engram, with no trailing slash.
|
||||
fn registry_url() -> String {
|
||||
let u: String = config("ENGRAM_URL")
|
||||
if str_eq(u, "") { return "http://localhost:8742" }
|
||||
// Strip trailing slash if present
|
||||
let n: Int = str_len(u)
|
||||
if str_ends_with(u, "/") {
|
||||
return str_slice(u, 0, n - 1)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// ── Publish ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_publish stores a vessel node in Engram.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name (e.g. "el-auth")
|
||||
// version — semver string (e.g. "0.1.0")
|
||||
// description — human-readable description
|
||||
// entry — build entry file path
|
||||
// deps_json — JSON array of dep objects (from manifest_deps)
|
||||
//
|
||||
// Returns the created node ID on success, "" on failure.
|
||||
fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String {
|
||||
let label: String = "vessel:" + name + ":" + version
|
||||
|
||||
// Build the content JSON — all vessel metadata in one blob
|
||||
let esc_name: String = json_escape_string(name)
|
||||
let esc_ver: String = json_escape_string(version)
|
||||
let esc_desc: String = json_escape_string(description)
|
||||
let esc_entry: String = json_escape_string(entry)
|
||||
let content_json: String = "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "}"
|
||||
|
||||
// Escape the content blob for embedding in the outer JSON
|
||||
let esc_label: String = json_escape_string(label)
|
||||
let esc_content: String = json_escape_string(content_json)
|
||||
|
||||
let body: String = "{\"label\":\"" + esc_label + "\",\"content\":\"" + esc_content + "\",\"node_type\":\"Entity\",\"salience\":0.9}"
|
||||
let url: String = registry_url() + "/api/nodes"
|
||||
|
||||
let resp: String = http_post_json(url, body)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + url)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
let err: String = json_get_string(resp, "error")
|
||||
if !str_eq(err, "") {
|
||||
println("epm: error from Engram: " + err)
|
||||
return ""
|
||||
}
|
||||
|
||||
let node_id: String = json_get_string(resp, "id")
|
||||
return node_id
|
||||
}
|
||||
|
||||
// ── Find ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_find searches Engram for a specific vessel by name and version.
|
||||
//
|
||||
// Returns the content JSON blob for the vessel, or "" if not found.
|
||||
// When version is "" any version matching the name is accepted (first hit).
|
||||
fn registry_find(name: String, version: String) -> String {
|
||||
let query: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let query = "vessel:" + name + ":" + version
|
||||
}
|
||||
|
||||
let enc_q: String = url_encode(query)
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=10"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response is a JSON array of node objects
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "" }
|
||||
|
||||
// Walk results to find exact match
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let node_label: String = json_get_string(node, "label")
|
||||
let expected_label: String = "vessel:" + name
|
||||
if !str_eq(version, "") {
|
||||
let expected_label = "vessel:" + name + ":" + version
|
||||
}
|
||||
if str_eq(node_label, expected_label) {
|
||||
// The content field is an escaped JSON blob — decode it
|
||||
let raw_content: String = json_get_string(node, "content")
|
||||
return raw_content
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// No exact match; if version was unspecified return first result's content
|
||||
if str_eq(version, "") {
|
||||
let first: String = json_array_get(resp, 0)
|
||||
let first_label: String = json_get_string(first, "label")
|
||||
if str_starts_with(first_label, "vessel:" + name + ":") {
|
||||
return json_get_string(first, "content")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_list returns all vessels from Engram as a JSON array.
|
||||
//
|
||||
// Each element in the returned array is a content JSON blob.
|
||||
// Returns "[]" when no vessels are found or Engram is unreachable.
|
||||
fn registry_list() -> String {
|
||||
let enc_q: String = url_encode("vessel:")
|
||||
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=200"
|
||||
|
||||
let resp: String = http_get(url)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + registry_url())
|
||||
return "[]"
|
||||
}
|
||||
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "[]" }
|
||||
|
||||
// Collect content blobs for nodes whose label starts with "vessel:"
|
||||
let out: String = "["
|
||||
let added: Int = 0
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let node: String = json_array_get(resp, i)
|
||||
let lbl: String = json_get_string(node, "label")
|
||||
if str_starts_with(lbl, "vessel:") {
|
||||
let content: String = json_get_string(node, "content")
|
||||
let esc: String = json_escape_string(content)
|
||||
if added == 0 {
|
||||
let out = out + "\"" + esc + "\""
|
||||
} else {
|
||||
let out = out + ",\"" + esc + "\""
|
||||
}
|
||||
let added = added + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return out + "]"
|
||||
}
|
||||
Reference in New Issue
Block a user