From 9e8d23bcd9bf9d1b169efc3cee55a06b8a72177c Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 19:31:24 -0500 Subject: [PATCH] =?UTF-8?q?add=20epm=20=E2=80=94=20El=20Package=20Manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::" - epm/src/install.el — topological dependency resolver with cycle detection; installs to .epm/vessels// - epm/src/epm.el — main entry point: publish / install / list / info --- epm/manifest.el | 17 ++++ epm/src/epm.el | 213 +++++++++++++++++++++++++++++++++++++++++ epm/src/install.el | 226 ++++++++++++++++++++++++++++++++++++++++++++ epm/src/manifest.el | 175 ++++++++++++++++++++++++++++++++++ epm/src/registry.el | 170 +++++++++++++++++++++++++++++++++ 5 files changed, 801 insertions(+) create mode 100644 epm/manifest.el create mode 100644 epm/src/epm.el create mode 100644 epm/src/install.el create mode 100644 epm/src/manifest.el create mode 100644 epm/src/registry.el diff --git a/epm/manifest.el b/epm/manifest.el new file mode 100644 index 0000000..e62733e --- /dev/null +++ b/epm/manifest.el @@ -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 "] + edition "2026" +} + +build { + entry "src/epm.el" + output "dist/" +} diff --git a/epm/src/epm.el b/epm/src/epm.el new file mode 100644 index 0000000..efe0beb --- /dev/null +++ b/epm/src/epm.el @@ -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 # install a specific vessel +// epm list # list all vessels in the registry +// epm info # show metadata for a vessel +// epm info # 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 or install @ +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 [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 install a specific vessel") + println(" epm install @ install a specific vessel at a version") + println(" epm list list all vessels in the registry") + println(" epm info show vessel metadata") + println(" epm info 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) +} diff --git a/epm/src/install.el b/epm/src/install.el new file mode 100644 index 0000000..50a3f1e --- /dev/null +++ b/epm/src/install.el @@ -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// +// +// 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 { + 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//. +// +// 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") +} diff --git a/epm/src/manifest.el b/epm/src/manifest.el new file mode 100644 index 0000000..fd3d636 --- /dev/null +++ b/epm/src/manifest.el @@ -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 +} diff --git a/epm/src/registry.el b/epm/src/registry.el new file mode 100644 index 0000000..5d7bbd6 --- /dev/null +++ b/epm/src/registry.el @@ -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::" (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 + "]" +}