Files
el/epm/src/manifest.el
Will Anderson 9e8d23bcd9 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
2026-05-04 19:31:24 -05:00

176 lines
5.8 KiB
EmacsLisp

// 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
}