Merge PR #1: ci: fix gen2/gen3 gcc flags and step name formatting
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: El SDK CI — dev
|
||||
name: El SDK CI - dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -27,18 +27,21 @@ jobs:
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
|
||||
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
@@ -46,38 +49,38 @@ jobs:
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
- name: Run tests — text
|
||||
- name: Run tests - text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
- name: Run tests - calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
- name: Run tests - time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
- name: Run tests - html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests — native (core)
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -87,7 +90,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
- name: Run tests — native (text)
|
||||
- name: Run tests - native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -97,7 +100,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
- name: Run tests — native (string)
|
||||
- name: Run tests - native (string)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -107,7 +110,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
- name: Run tests — native (math)
|
||||
- name: Run tests - native (math)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -117,7 +120,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
- name: Run tests — native (state)
|
||||
- name: Run tests - native (state)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -127,7 +130,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
- name: Run tests — native (time)
|
||||
- name: Run tests - native (time)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -137,7 +140,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
- name: Run tests — native (json)
|
||||
- name: Run tests - native (json)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -147,7 +150,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
- name: Run tests — native (env)
|
||||
- name: Run tests - native (env)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
@@ -157,7 +160,7 @@ jobs:
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
- name: Run tests — native (fs)
|
||||
- name: Run tests - native (fs)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
|
||||
+119
-52
@@ -1,31 +1,42 @@
|
||||
// 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/.
|
||||
// epm manages vessels: versioned El source packages backed by Gitea releases.
|
||||
// Each vessel corresponds to a Gitea repository under a shared org. Versions
|
||||
// are Gitea release tags; source archives are attached as release assets named
|
||||
// "<name>-<version>.tar.gz".
|
||||
//
|
||||
// Vessels are installed to ~/.el/packages/<name>/<version>/.
|
||||
// The installed registry lives at ~/.el/packages/installed.json.
|
||||
//
|
||||
// Usage:
|
||||
// epm publish # publish current project to registry
|
||||
// epm install # install all dependencies from manifest.el
|
||||
// epm install <vessel> # install a specific vessel
|
||||
// epm list # list all vessels in the registry
|
||||
// epm install <vessel> # install a specific vessel (latest)
|
||||
// epm install <vessel>@<ver> # install a specific vessel at a version
|
||||
// epm list # list installed vessels
|
||||
// epm info <vessel> # show metadata for a vessel
|
||||
// epm info <vessel> <version> # show metadata for a specific version
|
||||
// epm update # update all installed vessels
|
||||
// epm update <vessel> # update a specific vessel
|
||||
// epm check # check for available updates (no install)
|
||||
//
|
||||
// Configuration (environment):
|
||||
// ENGRAM_URL — Engram base URL (default: http://localhost:8742)
|
||||
// EPM_GITEA_API — Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)
|
||||
// EPM_REGISTRY_ORG — org hosting vessel repos (default: neuron-technologies)
|
||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||
//
|
||||
// Import order: manifest.el, registry.el, install.el must be compiled first.
|
||||
// This file imports all three and is the build entry point.
|
||||
// Import order: manifest.el, registry.el, install.el, update.el must be
|
||||
// compiled first. This file imports all four and is the build entry point.
|
||||
|
||||
import "manifest.el"
|
||||
import "registry.el"
|
||||
import "install.el"
|
||||
import "update.el"
|
||||
|
||||
// ── Subcommand handlers ───────────────────────────────────────────────────────
|
||||
|
||||
// cmd_publish reads manifest.el from the current directory and publishes
|
||||
// the vessel/package to the Engram registry.
|
||||
// the vessel to the Gitea registry as a release.
|
||||
fn cmd_publish() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
@@ -51,13 +62,13 @@ fn cmd_publish() -> Void {
|
||||
|
||||
println("epm: publishing " + name + " " + version + " ...")
|
||||
|
||||
let node_id: String = registry_publish(name, version, description, entry, deps_json)
|
||||
if str_eq(node_id, "") {
|
||||
let release_id: String = registry_publish(name, version, description, entry, deps_json)
|
||||
if str_eq(release_id, "") {
|
||||
println("epm: publish failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("epm: published " + name + " " + version + " (node: " + node_id + ")")
|
||||
println("epm: published " + name + " " + version + " (release: " + release_id + ")")
|
||||
}
|
||||
|
||||
// cmd_install_named installs a specific vessel by name (with optional version).
|
||||
@@ -74,41 +85,73 @@ fn cmd_install_named(spec: String) -> Void {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Notify about other available updates after a successful install
|
||||
let outdated: Int = check_updates()
|
||||
if outdated > 0 {
|
||||
println("")
|
||||
println(" note: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_list prints all vessels registered in Engram.
|
||||
// cmd_list prints all locally installed vessels.
|
||||
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")
|
||||
let installed: String = read_installed()
|
||||
if str_eq(installed, "") {
|
||||
println("epm: no vessels installed")
|
||||
return
|
||||
}
|
||||
if str_eq(installed, "{}") {
|
||||
println("epm: no vessels installed")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: " + native_int_to_str(n) + " vessel(s) in registry:")
|
||||
let inner_len: Int = str_len(installed)
|
||||
if inner_len < 2 {
|
||||
println("epm: no vessels installed")
|
||||
return
|
||||
}
|
||||
let body: String = str_slice(installed, 1, inner_len - 1)
|
||||
let body_trimmed: String = str_trim(body)
|
||||
if str_eq(body_trimmed, "") {
|
||||
println("epm: no vessels installed")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: installed vessels:")
|
||||
println("")
|
||||
|
||||
let pairs: [String] = str_split(body_trimmed, ",")
|
||||
let pair_count: Int = native_list_len(pairs)
|
||||
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)
|
||||
while i < pair_count {
|
||||
let pair: String = str_trim(native_list_get(pairs, i))
|
||||
let colon: Int = str_index_of(pair, ":")
|
||||
if colon > 0 {
|
||||
let raw_key: String = str_trim(str_slice(pair, 0, colon))
|
||||
let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
|
||||
let key_len: Int = str_len(raw_key)
|
||||
let val_len: Int = str_len(raw_val)
|
||||
let vname2: String = raw_key
|
||||
let vver2: String = raw_val
|
||||
if str_starts_with(raw_key, "\"") {
|
||||
let vname2 = str_slice(raw_key, 1, key_len - 1)
|
||||
}
|
||||
if str_starts_with(raw_val, "\"") {
|
||||
let vver2 = str_slice(raw_val, 1, val_len - 1)
|
||||
}
|
||||
println(" " + vname2 + " " + vver2)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_info prints detailed metadata for a specific vessel.
|
||||
// cmd_info prints detailed metadata for a specific vessel from the registry.
|
||||
// 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)
|
||||
@@ -132,27 +175,18 @@ fn cmd_info(argv: [String], idx: Int) -> Void {
|
||||
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")
|
||||
let dl_url: String = json_get_string(content, "download_url")
|
||||
|
||||
println("name: " + name)
|
||||
println("version: " + ver)
|
||||
println("description: " + desc)
|
||||
println("entry: " + entry)
|
||||
println("download: " + dl_url)
|
||||
|
||||
let dep_n: Int = json_array_len(deps_raw)
|
||||
if dep_n == 0 {
|
||||
println("dependencies: (none)")
|
||||
let local_ver: String = installed_version(name)
|
||||
if !str_eq(local_ver, "") {
|
||||
println("installed: " + local_ver)
|
||||
} 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
|
||||
}
|
||||
println("installed: (not installed)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,16 +201,21 @@ fn main() -> Void {
|
||||
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 <vessel> install a specific vessel")
|
||||
println(" epm install <vessel>@<ver> install a specific vessel at a version")
|
||||
println(" epm list list all vessels in the registry")
|
||||
println(" epm info <vessel> show vessel metadata")
|
||||
println(" epm info <vessel> <version> show metadata for a specific version")
|
||||
println(" epm publish publish current project to registry")
|
||||
println(" epm install install all dependencies (from manifest.el)")
|
||||
println(" epm install <vessel> install a specific vessel (latest version)")
|
||||
println(" epm install <vessel>@<ver> install a specific vessel at a version")
|
||||
println(" epm list list installed vessels")
|
||||
println(" epm info <vessel> show vessel metadata from registry")
|
||||
println(" epm info <vessel> <version> show metadata for a specific version")
|
||||
println(" epm update update all installed vessels")
|
||||
println(" epm update <vessel> update a specific vessel")
|
||||
println(" epm check check for available updates (no install)")
|
||||
println("")
|
||||
println("configuration:")
|
||||
println(" ENGRAM_URL Engram base URL (default: http://localhost:8742)")
|
||||
println(" EPM_GITEA_API Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)")
|
||||
println(" EPM_REGISTRY_ORG org hosting vessel repos (default: neuron-technologies)")
|
||||
println(" EPM_TOKEN Gitea personal access token (required for publish)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
@@ -193,6 +232,12 @@ fn main() -> Void {
|
||||
cmd_install_named(spec)
|
||||
} else {
|
||||
cmd_install()
|
||||
// Notify about other available updates after a successful install
|
||||
let outdated: Int = check_updates()
|
||||
if outdated > 0 {
|
||||
println("")
|
||||
println(" note: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -207,6 +252,28 @@ fn main() -> Void {
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "update") {
|
||||
if argc >= 3 {
|
||||
let vessel_arg: String = native_list_get(argv, 2)
|
||||
cmd_update(vessel_arg)
|
||||
} else {
|
||||
cmd_update("")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if str_eq(sub, "check") {
|
||||
println("epm: checking for updates ...")
|
||||
let outdated: Int = check_updates()
|
||||
if outdated == 0 {
|
||||
println("epm: all installed vessels are up to date")
|
||||
} else {
|
||||
println("")
|
||||
println("epm: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: unknown subcommand '" + sub + "'")
|
||||
println("run 'epm' with no arguments for usage")
|
||||
exit(1)
|
||||
|
||||
+229
-49
@@ -1,22 +1,175 @@
|
||||
// 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).
|
||||
// by fetching each dep from the Gitea registry, detects cycles, and produces
|
||||
// a topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
// Installed vessels land in: ~/.el/packages/<name>/<version>/
|
||||
// Installed tracking: ~/.el/packages/installed.json
|
||||
//
|
||||
// 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
|
||||
// 2. For each dep, fetch its metadata from the Gitea registry
|
||||
// 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) ──────────────────────────────────
|
||||
// ── Install paths ─────────────────────────────────────────────────────────────
|
||||
|
||||
// packages_dir returns the root directory for installed vessels.
|
||||
fn packages_dir() -> String {
|
||||
let home: String = env("HOME")
|
||||
return home + "/.el/packages"
|
||||
}
|
||||
|
||||
// installed_json_path returns the path to the installed-vessel tracker.
|
||||
fn installed_json_path() -> String {
|
||||
return packages_dir() + "/installed.json"
|
||||
}
|
||||
|
||||
// ── Installed tracking ────────────────────────────────────────────────────────
|
||||
|
||||
// read_installed reads installed.json and returns it as a JSON object string.
|
||||
// Returns "{}" if the file is missing or empty.
|
||||
fn read_installed() -> String {
|
||||
let path: String = installed_json_path()
|
||||
let content: String = fs_read(path)
|
||||
if str_eq(content, "") { return "{}" }
|
||||
let trimmed: String = str_trim(content)
|
||||
if str_eq(trimmed, "") { return "{}" }
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// write_installed writes a JSON object string to installed.json.
|
||||
fn write_installed(content: String) -> Void {
|
||||
let path: String = installed_json_path()
|
||||
fs_write(path, content)
|
||||
}
|
||||
|
||||
// installed_version returns the currently installed version of a vessel,
|
||||
// or "" if it is not installed.
|
||||
fn installed_version(name: String) -> String {
|
||||
let installed: String = read_installed()
|
||||
return json_get_string(installed, name)
|
||||
}
|
||||
|
||||
// json_set_key inserts or replaces a string key/value pair in a JSON object.
|
||||
// The object is expected to be a flat { "k":"v", ... } structure.
|
||||
// Returns the updated JSON object string.
|
||||
fn json_set_key(obj: String, key: String, value: String) -> String {
|
||||
let esc_key: String = json_escape_string(key)
|
||||
let esc_val: String = json_escape_string(value)
|
||||
let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\""
|
||||
let search_key: String = "\"" + esc_key + "\":"
|
||||
|
||||
// If the key already exists, replace its value in-place.
|
||||
let key_pos: Int = str_index_of(obj, search_key)
|
||||
if key_pos >= 0 {
|
||||
// Find start of value: skip past search_key
|
||||
let val_start: Int = key_pos + str_len(search_key)
|
||||
let after_colon: String = str_slice(obj, val_start, str_len(obj))
|
||||
let trimmed_after: String = str_trim(after_colon)
|
||||
|
||||
// The value is a quoted string — find its extent
|
||||
let q1: Int = str_index_of(trimmed_after, "\"")
|
||||
if q1 >= 0 {
|
||||
let inside: String = str_slice(trimmed_after, q1 + 1, str_len(trimmed_after))
|
||||
let q2: Int = str_index_of(inside, "\"")
|
||||
if q2 >= 0 {
|
||||
// Reconstruct: prefix + new pair + suffix
|
||||
let prefix: String = str_slice(obj, 0, key_pos)
|
||||
// after the closing quote of the old value
|
||||
let old_val_end_in_inside: Int = q2 + 1
|
||||
// offset back to full string: key_pos + str_len(search_key) + leading whitespace + q1 + 1 + q2 + 1
|
||||
// Simpler: rebuild from scratch without the old entry, then inject
|
||||
// Strategy: remove old entry by rebuilding pairs
|
||||
let rebuilt: String = json_remove_key(obj, key)
|
||||
return json_append_key(rebuilt, esc_key, esc_val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key does not exist — append it
|
||||
return json_append_key(obj, esc_key, esc_val)
|
||||
}
|
||||
|
||||
// json_remove_key removes a key/value pair from a flat JSON object string.
|
||||
// Returns the object without that key. Used internally by json_set_key.
|
||||
fn json_remove_key(obj: String, key: String) -> String {
|
||||
// Parse pairs manually and rebuild excluding the target key
|
||||
// Trim outer braces
|
||||
let inner: String = str_trim(obj)
|
||||
let inner_len: Int = str_len(inner)
|
||||
if inner_len < 2 { return "{}" }
|
||||
// Remove leading { and trailing }
|
||||
let body: String = str_slice(inner, 1, inner_len - 1)
|
||||
let body_trimmed: String = str_trim(body)
|
||||
if str_eq(body_trimmed, "") { return "{}" }
|
||||
|
||||
// Split on comma — note: this is a simple split that works for flat objects
|
||||
// with no nested commas (vessel names and versions have no commas)
|
||||
let pairs: [String] = str_split(body_trimmed, ",")
|
||||
let pair_count: Int = native_list_len(pairs)
|
||||
let result: String = "{"
|
||||
let added: Int = 0
|
||||
let i: Int = 0
|
||||
while i < pair_count {
|
||||
let pair: String = str_trim(native_list_get(pairs, i))
|
||||
// Each pair looks like "key":"value"
|
||||
// Extract the key portion
|
||||
let colon_pos: Int = str_index_of(pair, ":")
|
||||
if colon_pos > 0 {
|
||||
let raw_k: String = str_trim(str_slice(pair, 0, colon_pos))
|
||||
// raw_k is "key" — strip quotes
|
||||
let k_len: Int = str_len(raw_k)
|
||||
let k_unquoted: String = raw_k
|
||||
if str_starts_with(raw_k, "\"") {
|
||||
let k_unquoted = str_slice(raw_k, 1, k_len - 1)
|
||||
}
|
||||
if !str_eq(k_unquoted, key) {
|
||||
if added == 0 {
|
||||
let result = result + pair
|
||||
} else {
|
||||
let result = result + "," + pair
|
||||
}
|
||||
let added = added + 1
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return result + "}"
|
||||
}
|
||||
|
||||
// json_append_key appends an already-escaped key/value pair to a JSON object.
|
||||
fn json_append_key(obj: String, esc_key: String, esc_val: String) -> String {
|
||||
let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\""
|
||||
let inner: String = str_trim(obj)
|
||||
let inner_len: Int = str_len(inner)
|
||||
if inner_len < 2 { return "{" + pair + "}" }
|
||||
// Remove trailing }
|
||||
let body: String = str_slice(inner, 0, inner_len - 1)
|
||||
let body_trimmed: String = str_trim(body)
|
||||
// Remove leading {
|
||||
let body2: String = str_slice(body_trimmed, 1, str_len(body_trimmed))
|
||||
let body2_trimmed: String = str_trim(body2)
|
||||
if str_eq(body2_trimmed, "") {
|
||||
return "{" + pair + "}"
|
||||
}
|
||||
return "{" + body2_trimmed + "," + pair + "}"
|
||||
}
|
||||
|
||||
// record_installed updates installed.json to record name -> version.
|
||||
fn record_installed(name: String, version: String) -> Void {
|
||||
let mk_ret: Int = exec_command("mkdir -p " + packages_dir())
|
||||
let installed: String = read_installed()
|
||||
let updated: String = json_set_key(installed, name, version)
|
||||
write_installed(updated)
|
||||
}
|
||||
|
||||
// ── List helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
// list_contains returns true if item appears in lst.
|
||||
fn list_contains(lst: [String], item: String) -> Bool {
|
||||
@@ -32,12 +185,12 @@ fn list_contains(lst: [String], item: String) -> Bool {
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
// resolve_deps_recursive fetches a vessel's metadata from the registry and
|
||||
// walks its dependency list depth-first, building a topological order.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name to resolve
|
||||
// version — version string (may be "" for any)
|
||||
// version — version string (may be "" for latest)
|
||||
// chain — names currently on the recursion stack (cycle detection)
|
||||
// visited — names already appended to order (dedup)
|
||||
// order — accumulated result (list of "name:version" strings)
|
||||
@@ -60,38 +213,22 @@ fn resolve_deps_recursive(name: String, version: String, chain: [String], visite
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
// Fetch vessel metadata from the registry
|
||||
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":[...]}
|
||||
// content is a JSON blob: {"name":..., "version":..., "description":..., "download_url":...}
|
||||
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")
|
||||
// registry_find returns top-level metadata only; deps are not included in
|
||||
// the Gitea-backed registry response (they live in the release body comment).
|
||||
// For now we skip transitive dep resolution — install only direct deps.
|
||||
|
||||
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)
|
||||
// Append this vessel (after all its deps would go)
|
||||
let visited = native_list_append(visited, actual_key)
|
||||
let order = native_list_append(order, actual_key)
|
||||
|
||||
@@ -144,29 +281,72 @@ fn resolve_install_order() -> [String] {
|
||||
|
||||
// ── 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.
|
||||
//
|
||||
// install_vessel downloads and extracts a vessel to ~/.el/packages/<name>/<version>/.
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
// Resolve version if not specified
|
||||
let ver: String = version
|
||||
if str_eq(ver, "") {
|
||||
let ver = registry_latest_version(name)
|
||||
if str_eq(ver, "") {
|
||||
println("epm: error: no releases found for vessel '" + name + "'")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
let current: String = installed_version(name)
|
||||
if str_eq(current, ver) {
|
||||
println("epm: " + name + " " + ver + " is already installed")
|
||||
return true
|
||||
}
|
||||
|
||||
// Fetch metadata from registry
|
||||
let content: String = registry_find(name, ver)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry")
|
||||
println("epm: error: cannot fetch '" + name + "' '" + ver + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
let download_url: String = json_get_string(content, "download_url")
|
||||
if str_eq(download_url, "") {
|
||||
println("epm: error: no download URL for '" + name + "' '" + ver + "'")
|
||||
return false
|
||||
}
|
||||
|
||||
// Create install directory
|
||||
let install_dir: String = packages_dir() + "/" + name + "/" + ver
|
||||
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)
|
||||
// Download tarball to /tmp
|
||||
let tmp: String = "/tmp/epm-" + name + "-" + ver + ".tar.gz"
|
||||
let token: String = registry_token()
|
||||
let dl_ret: Int = 0
|
||||
if str_eq(token, "") {
|
||||
let dl_ret = exec_command("curl -sL -o " + tmp + " \"" + download_url + "\"")
|
||||
} else {
|
||||
let dl_ret = exec_command("curl -sL -H \"Authorization: token " + token + "\" -o " + tmp + " \"" + download_url + "\"")
|
||||
}
|
||||
if dl_ret != 0 {
|
||||
println("epm: error: download failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(dl_ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
// Extract tarball
|
||||
let extract_ret: Int = exec_command("tar -xzf " + tmp + " -C " + install_dir + " --strip-components=1")
|
||||
if extract_ret != 0 {
|
||||
println("epm: error: extraction failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(extract_ret) + ")")
|
||||
let rm_ret: Int = exec_command("rm -f " + tmp)
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove temp file
|
||||
let rm_ret: Int = exec_command("rm -f " + tmp)
|
||||
|
||||
// Record installation
|
||||
record_installed(name, ver)
|
||||
|
||||
println("epm: installed " + name + " " + ver + " -> " + install_dir)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -199,21 +379,21 @@ fn cmd_install() -> Void {
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
// Ensure packages directory exists
|
||||
let mk2: Int = exec_command("mkdir -p " + packages_dir())
|
||||
|
||||
// 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
|
||||
// Split "name:version" on the first 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 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)
|
||||
|
||||
+200
-132
@@ -1,26 +1,25 @@
|
||||
// epm/src/registry.el — Engram-backed vessel registry
|
||||
// epm/src/registry.el — Gitea-backed vessel registry
|
||||
//
|
||||
// Vessels are stored in Engram as nodes with a structured label scheme:
|
||||
// Vessels are stored as Gitea repositories under a shared org:
|
||||
//
|
||||
// label: "vessel:<name>:<version>" (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)
|
||||
// repo: <org>/<name> at https://git.neuralplatform.ai
|
||||
// versions: Gitea release tags (e.g. "0.1.0")
|
||||
// archive: release asset named "<name>-<version>.tar.gz"
|
||||
//
|
||||
// All registry operations go over HTTP to Engram. The Engram URL is read
|
||||
// from the ENGRAM_URL environment variable; defaults to http://localhost:8742.
|
||||
// All registry read operations use unauthenticated HTTP GET.
|
||||
// registry_publish requires EPM_TOKEN to be set.
|
||||
//
|
||||
// Endpoints used:
|
||||
// POST /api/nodes — publish a vessel node
|
||||
// GET /api/search?q=... — find vessels by label prefix
|
||||
// Configuration (environment variables):
|
||||
// EPM_GITEA_API — Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)
|
||||
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||
|
||||
// ── Engram URL ────────────────────────────────────────────────────────────────
|
||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// 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
|
||||
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
||||
fn registry_api_url() -> String {
|
||||
let u: String = config("EPM_GITEA_API")
|
||||
if str_eq(u, "") { return "https://git.neuralplatform.ai/api/v1" }
|
||||
let n: Int = str_len(u)
|
||||
if str_ends_with(u, "/") {
|
||||
return str_slice(u, 0, n - 1)
|
||||
@@ -28,143 +27,212 @@ fn registry_url() -> String {
|
||||
return u
|
||||
}
|
||||
|
||||
// registry_org returns the Gitea org that hosts vessel repos.
|
||||
fn registry_org() -> String {
|
||||
let o: String = config("EPM_REGISTRY_ORG")
|
||||
if str_eq(o, "") { return "neuron-technologies" }
|
||||
return o
|
||||
}
|
||||
|
||||
// registry_token returns the Gitea auth token for publish operations.
|
||||
fn registry_token() -> String {
|
||||
return config("EPM_TOKEN")
|
||||
}
|
||||
|
||||
// ── Release lookups ───────────────────────────────────────────────────────────
|
||||
|
||||
// registry_find_release fetches a specific release by tag from Gitea.
|
||||
// GET /api/v1/repos/<org>/<name>/releases/tags/<version>
|
||||
// Returns the raw release JSON object, or "" if not found / on error.
|
||||
fn registry_find_release(name: String, version: String) -> String {
|
||||
let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases/tags/" + version
|
||||
let token: String = registry_token()
|
||||
let resp: String = ""
|
||||
if str_eq(token, "") {
|
||||
let resp = http_get(url)
|
||||
} else {
|
||||
let headers: String = "Authorization: token " + token
|
||||
let resp = http_get_with_headers(url, headers)
|
||||
}
|
||||
if str_eq(resp, "") { return "" }
|
||||
// Gitea returns 404 as an error JSON or empty — check for "id" field
|
||||
let release_id: String = json_get_string(resp, "id")
|
||||
if str_eq(release_id, "") { return "" }
|
||||
return resp
|
||||
}
|
||||
|
||||
// registry_list_releases fetches all releases for a vessel from Gitea.
|
||||
// GET /api/v1/repos/<org>/<name>/releases
|
||||
// Returns a JSON array of release objects, or "[]" on error.
|
||||
fn registry_list_releases(name: String) -> String {
|
||||
let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases?limit=50"
|
||||
let token: String = registry_token()
|
||||
let resp: String = ""
|
||||
if str_eq(token, "") {
|
||||
let resp = http_get(url)
|
||||
} else {
|
||||
let headers: String = "Authorization: token " + token
|
||||
let resp = http_get_with_headers(url, headers)
|
||||
}
|
||||
if str_eq(resp, "") { return "[]" }
|
||||
// Verify it is an array
|
||||
let count: Int = json_array_len(resp)
|
||||
if count < 0 { return "[]" }
|
||||
return resp
|
||||
}
|
||||
|
||||
// registry_latest_version returns the tag_name of the most recent release.
|
||||
// Gitea returns releases sorted by creation date (newest first).
|
||||
// Returns "" if there are no releases or the repo is missing.
|
||||
fn registry_latest_version(name: String) -> String {
|
||||
let releases: String = registry_list_releases(name)
|
||||
let count: Int = json_array_len(releases)
|
||||
if count == 0 { return "" }
|
||||
let first: String = json_array_get(releases, 0)
|
||||
return json_get_string(first, "tag_name")
|
||||
}
|
||||
|
||||
// registry_find_asset_url scans a release JSON object for the source tarball.
|
||||
// Looks for an asset named "<name>-<version>.tar.gz".
|
||||
// Falls back to zipball_url if no matching asset is found.
|
||||
// Returns "" if the release JSON is empty.
|
||||
fn registry_find_asset_url(release_json: String, name: String, version: String) -> String {
|
||||
if str_eq(release_json, "") { return "" }
|
||||
|
||||
let target_asset: String = name + "-" + version + ".tar.gz"
|
||||
let assets_json: String = json_get_raw(release_json, "assets")
|
||||
let asset_count: Int = json_array_len(assets_json)
|
||||
|
||||
let i: Int = 0
|
||||
while i < asset_count {
|
||||
let asset: String = json_array_get(assets_json, i)
|
||||
let asset_name: String = json_get_string(asset, "name")
|
||||
if str_eq(asset_name, target_asset) {
|
||||
return json_get_string(asset, "browser_download_url")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Fall back to zipball_url from the release
|
||||
let zipball: String = json_get_string(release_json, "zipball_url")
|
||||
return zipball
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_find looks up a vessel in the Gitea registry.
|
||||
// When version is "" the latest release is used.
|
||||
//
|
||||
// Returns a metadata JSON blob with keys:
|
||||
// name, version, description, download_url
|
||||
// Returns "" if the vessel or version is not found.
|
||||
fn registry_find(name: String, version: String) -> String {
|
||||
let ver: String = version
|
||||
if str_eq(ver, "") {
|
||||
let ver = registry_latest_version(name)
|
||||
if str_eq(ver, "") {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
let release_json: String = registry_find_release(name, ver)
|
||||
if str_eq(release_json, "") { return "" }
|
||||
|
||||
let description: String = json_get_string(release_json, "body")
|
||||
let download_url: String = registry_find_asset_url(release_json, name, ver)
|
||||
|
||||
let esc_name: String = json_escape_string(name)
|
||||
let esc_ver: String = json_escape_string(ver)
|
||||
let esc_desc: String = json_escape_string(description)
|
||||
let esc_url: String = json_escape_string(download_url)
|
||||
|
||||
return "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"download_url\":\"" + esc_url + "\"}"
|
||||
}
|
||||
|
||||
// registry_list returns a JSON array of installed vessel metadata blobs.
|
||||
// Listing all org repos requires auth in many Gitea setups, so this
|
||||
// falls back to reading the local installed.json.
|
||||
fn registry_list() -> String {
|
||||
let installed: String = read_installed()
|
||||
// installed is a JSON object: {"name":"version",...}
|
||||
// We return an empty array — callers should use installed_version() per name
|
||||
if str_eq(installed, "") { return "[]" }
|
||||
if str_eq(installed, "{}") { return "[]" }
|
||||
return "[]"
|
||||
}
|
||||
|
||||
// ── Publish ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_publish stores a vessel node in Engram.
|
||||
// registry_publish creates a Gitea release and uploads the source tarball.
|
||||
//
|
||||
// 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)
|
||||
// Steps:
|
||||
// 1. Build a tarball of the current directory (excluding .epm/, .git/)
|
||||
// 2. POST /repos/<org>/<name>/releases to create the release
|
||||
// 3. POST /repos/<org>/<name>/releases/<id>/assets to upload the tarball
|
||||
//
|
||||
// Returns the created node ID on success, "" on failure.
|
||||
// Returns the release ID string 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
|
||||
let token: String = registry_token()
|
||||
if str_eq(token, "") {
|
||||
println("epm: error: EPM_TOKEN is required for publishing")
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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 tarball: String = "/tmp/epm-publish-" + name + "-" + version + ".tar.gz"
|
||||
let asset_name: String = name + "-" + version + ".tar.gz"
|
||||
|
||||
// Build the source tarball from the current directory
|
||||
let tar_cmd: String = "tar -czf " + tarball + " --exclude='.epm' --exclude='.git' --exclude='*.tar.gz' ."
|
||||
let tar_ret: Int = exec_command(tar_cmd)
|
||||
if tar_ret != 0 {
|
||||
println("epm: error: failed to create source tarball (exit " + native_int_to_str(tar_ret) + ")")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Build release body — include metadata as JSON comment
|
||||
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 + "}"
|
||||
let body_text: String = description + "\n\n<!-- epm: {\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "} -->"
|
||||
let esc_body: String = json_escape_string(body_text)
|
||||
let esc_tag: String = json_escape_string(version)
|
||||
let esc_name_field: String = json_escape_string(name + " " + version)
|
||||
|
||||
// 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 release_body: String = "{\"tag_name\":\"" + esc_tag + "\",\"name\":\"" + esc_name_field + "\",\"body\":\"" + esc_body + "\",\"draft\":false,\"prerelease\":false}"
|
||||
|
||||
let body: String = "{\"label\":\"" + esc_label + "\",\"content\":\"" + esc_content + "\",\"node_type\":\"Entity\",\"salience\":0.9}"
|
||||
let url: String = registry_url() + "/api/nodes"
|
||||
let api_base: String = registry_api_url()
|
||||
let org: String = registry_org()
|
||||
let releases_url: String = api_base + "/repos/" + org + "/" + name + "/releases"
|
||||
let auth_header: String = "Authorization: token " + token
|
||||
|
||||
let resp: String = http_post_json(url, body)
|
||||
let resp: String = http_post_json_with_headers(releases_url, auth_header, release_body)
|
||||
if str_eq(resp, "") {
|
||||
println("epm: error: Engram unreachable at " + url)
|
||||
println("epm: error: failed to create release on Gitea (no response)")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
let err: String = json_get_string(resp, "error")
|
||||
if !str_eq(err, "") {
|
||||
println("epm: error from Engram: " + err)
|
||||
let err_msg: String = json_get_string(resp, "message")
|
||||
if !str_eq(err_msg, "") {
|
||||
println("epm: error from Gitea: " + err_msg)
|
||||
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())
|
||||
let release_id: String = json_get_string(resp, "id")
|
||||
if str_eq(release_id, "") {
|
||||
println("epm: error: Gitea release response missing id")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response is a JSON array of node objects
|
||||
let count: Int = json_array_len(resp)
|
||||
if count == 0 { return "" }
|
||||
// Upload the tarball as a release asset via curl (multipart form)
|
||||
let upload_url: String = api_base + "/repos/" + org + "/" + name + "/releases/" + release_id + "/assets?name=" + asset_name
|
||||
let curl_cmd: String = "curl -s -X POST -H \"Authorization: token " + token + "\" -H \"Content-Type: application/octet-stream\" --data-binary @" + tarball + " \"" + upload_url + "\""
|
||||
let upload_resp: String = exec(curl_cmd)
|
||||
|
||||
// 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
|
||||
// Clean up tarball
|
||||
let rm_ret: Int = exec_command("rm -f " + tarball)
|
||||
|
||||
if str_eq(upload_resp, "") {
|
||||
println("epm: warning: asset upload returned no response (release " + release_id + " was created)")
|
||||
}
|
||||
|
||||
// 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 + "]"
|
||||
return release_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// epm/src/update.el — version checking and auto-update detection
|
||||
//
|
||||
// Compares installed vessel versions against the latest available releases
|
||||
// in the Gitea registry, and optionally upgrades them.
|
||||
//
|
||||
// Depends on: registry.el (registry_latest_version, registry_find),
|
||||
// install.el (read_installed, install_vessel, installed_version)
|
||||
|
||||
// ── Semver helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// semver_part extracts the Nth dot-separated component from a semver string.
|
||||
// Returns "0" if the component is missing.
|
||||
fn semver_part(ver: String, idx: Int) -> String {
|
||||
let parts: [String] = str_split(ver, ".")
|
||||
let n: Int = native_list_len(parts)
|
||||
if idx >= n { return "0" }
|
||||
let v: String = str_trim(native_list_get(parts, idx))
|
||||
if str_eq(v, "") { return "0" }
|
||||
return v
|
||||
}
|
||||
|
||||
// semver_compare compares two semver strings a and b.
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
|
||||
// Compares major, then minor, then patch numerically.
|
||||
fn semver_compare(a: String, b: String) -> Int {
|
||||
let a_major: Int = native_str_to_int(semver_part(a, 0))
|
||||
let b_major: Int = native_str_to_int(semver_part(b, 0))
|
||||
if a_major < b_major { return -1 }
|
||||
if a_major > b_major { return 1 }
|
||||
|
||||
let a_minor: Int = native_str_to_int(semver_part(a, 1))
|
||||
let b_minor: Int = native_str_to_int(semver_part(b, 1))
|
||||
if a_minor < b_minor { return -1 }
|
||||
if a_minor > b_minor { return 1 }
|
||||
|
||||
let a_patch: Int = native_str_to_int(semver_part(a, 2))
|
||||
let b_patch: Int = native_str_to_int(semver_part(b, 2))
|
||||
if a_patch < b_patch { return -1 }
|
||||
if a_patch > b_patch { return 1 }
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// semver_gt returns true if a is strictly greater than b.
|
||||
fn semver_gt(a: String, b: String) -> Bool {
|
||||
let cmp: Int = semver_compare(a, b)
|
||||
return cmp == 1
|
||||
}
|
||||
|
||||
// ── Update checks ─────────────────────────────────────────────────────────────
|
||||
|
||||
// check_updates scans installed.json and checks if newer versions are available
|
||||
// in the registry. Prints a notice for each outdated vessel.
|
||||
// Returns the count of outdated vessels.
|
||||
fn check_updates() -> Int {
|
||||
let installed: String = read_installed()
|
||||
if str_eq(installed, "") { return 0 }
|
||||
if str_eq(installed, "{}") { return 0 }
|
||||
|
||||
// installed is a flat JSON object: {"name":"version",...}
|
||||
// We need to iterate over its keys. Since El has no map-iteration primitive,
|
||||
// we parse the raw JSON object manually by splitting on commas.
|
||||
let inner_len: Int = str_len(installed)
|
||||
if inner_len < 2 { return 0 }
|
||||
// Strip outer braces
|
||||
let body: String = str_slice(installed, 1, inner_len - 1)
|
||||
let body_trimmed: String = str_trim(body)
|
||||
if str_eq(body_trimmed, "") { return 0 }
|
||||
|
||||
let pairs: [String] = str_split(body_trimmed, ",")
|
||||
let pair_count: Int = native_list_len(pairs)
|
||||
let outdated: Int = 0
|
||||
|
||||
let i: Int = 0
|
||||
while i < pair_count {
|
||||
let pair: String = str_trim(native_list_get(pairs, i))
|
||||
// pair looks like "name":"version"
|
||||
let colon: Int = str_index_of(pair, ":")
|
||||
if colon > 0 {
|
||||
let raw_key: String = str_trim(str_slice(pair, 0, colon))
|
||||
let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
|
||||
// Strip surrounding quotes
|
||||
let key_len: Int = str_len(raw_key)
|
||||
let val_len: Int = str_len(raw_val)
|
||||
let vessel_name: String = raw_key
|
||||
let vessel_ver: String = raw_val
|
||||
if str_starts_with(raw_key, "\"") {
|
||||
let vessel_name = str_slice(raw_key, 1, key_len - 1)
|
||||
}
|
||||
if str_starts_with(raw_val, "\"") {
|
||||
let vessel_ver = str_slice(raw_val, 1, val_len - 1)
|
||||
}
|
||||
|
||||
// Check latest version in registry
|
||||
let latest: String = registry_latest_version(vessel_name)
|
||||
if !str_eq(latest, "") {
|
||||
if semver_gt(latest, vessel_ver) {
|
||||
println(" " + vessel_name + ": " + vessel_ver + " -> " + latest + " available")
|
||||
let outdated = outdated + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
return outdated
|
||||
}
|
||||
|
||||
// ── Update command ────────────────────────────────────────────────────────────
|
||||
|
||||
// cmd_update fetches the latest version of all installed vessels (or a specific
|
||||
// one) and installs it if newer than what's currently installed.
|
||||
// If vessel_name is "" it updates all installed vessels.
|
||||
fn cmd_update(vessel_name: String) -> Void {
|
||||
if !str_eq(vessel_name, "") {
|
||||
// Update a single named vessel
|
||||
let current: String = installed_version(vessel_name)
|
||||
if str_eq(current, "") {
|
||||
println("epm: '" + vessel_name + "' is not installed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let latest: String = registry_latest_version(vessel_name)
|
||||
if str_eq(latest, "") {
|
||||
println("epm: no releases found for '" + vessel_name + "' in registry")
|
||||
return
|
||||
}
|
||||
|
||||
if !semver_gt(latest, current) {
|
||||
println("epm: " + vessel_name + " " + current + " is already up to date")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: updating " + vessel_name + " " + current + " -> " + latest + " ...")
|
||||
let ok: Bool = install_vessel(vessel_name, latest)
|
||||
if !ok {
|
||||
println("epm: update failed for " + vessel_name)
|
||||
exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update all installed vessels
|
||||
let installed: String = read_installed()
|
||||
if str_eq(installed, "") {
|
||||
println("epm: no installed vessels")
|
||||
return
|
||||
}
|
||||
if str_eq(installed, "{}") {
|
||||
println("epm: no installed vessels")
|
||||
return
|
||||
}
|
||||
|
||||
let inner_len: Int = str_len(installed)
|
||||
if inner_len < 2 {
|
||||
println("epm: no installed vessels")
|
||||
return
|
||||
}
|
||||
let body: String = str_slice(installed, 1, inner_len - 1)
|
||||
let body_trimmed: String = str_trim(body)
|
||||
if str_eq(body_trimmed, "") {
|
||||
println("epm: no installed vessels")
|
||||
return
|
||||
}
|
||||
|
||||
let pairs: [String] = str_split(body_trimmed, ",")
|
||||
let pair_count: Int = native_list_len(pairs)
|
||||
let updated_count: Int = 0
|
||||
|
||||
let i: Int = 0
|
||||
while i < pair_count {
|
||||
let pair: String = str_trim(native_list_get(pairs, i))
|
||||
let colon: Int = str_index_of(pair, ":")
|
||||
if colon > 0 {
|
||||
let raw_key: String = str_trim(str_slice(pair, 0, colon))
|
||||
let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
|
||||
let key_len: Int = str_len(raw_key)
|
||||
let val_len: Int = str_len(raw_val)
|
||||
let vessel_name2: String = raw_key
|
||||
let vessel_ver2: String = raw_val
|
||||
if str_starts_with(raw_key, "\"") {
|
||||
let vessel_name2 = str_slice(raw_key, 1, key_len - 1)
|
||||
}
|
||||
if str_starts_with(raw_val, "\"") {
|
||||
let vessel_ver2 = str_slice(raw_val, 1, val_len - 1)
|
||||
}
|
||||
|
||||
let latest: String = registry_latest_version(vessel_name2)
|
||||
if !str_eq(latest, "") {
|
||||
if semver_gt(latest, vessel_ver2) {
|
||||
println("epm: updating " + vessel_name2 + " " + vessel_ver2 + " -> " + latest + " ...")
|
||||
let ok: Bool = install_vessel(vessel_name2, latest)
|
||||
if ok {
|
||||
let updated_count = updated_count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
if updated_count == 0 {
|
||||
println("epm: all installed vessels are up to date")
|
||||
} else {
|
||||
println("epm: updated " + native_int_to_str(updated_count) + " vessel(s)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user