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,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
|
||||
}
|
||||
Reference in New Issue
Block a user