9e8d23bcd9
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
176 lines
5.8 KiB
EmacsLisp
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
|
|
}
|