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:
Will Anderson
2026-05-04 19:31:24 -05:00
parent 0791fda43e
commit 9e8d23bcd9
5 changed files with 801 additions and 0 deletions
+226
View File
@@ -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")
}