feat: El port — vessels populated alongside Rust

Adds src/main.el + manifest.el for each vessel in this workspace,
ported from the Rust sources during the El consolidation pass on
2026-04-30. Each vessel now has both Rust (legacy) and El (target)
sources side-by-side; Rust will be removed once the El paths are
verified at runtime, vessel by vessel.

Per-vessel work was split across multiple parallel agents reading the
Rust to understand intent, then designing idiomatic El. Not 1:1
transliteration. Each ported vessel includes:
  - manifest.el per spec/language.md \u00a715.1
  - src/main.el with the vessel's public surface
  - Compile verified via dist/platform/elc + cc against the el_runtime

Known gaps surfaced during the port (held for follow-up): HMAC-SHA256
and base64 crypto, HTTP status code in handler returns, request
headers in handler signatures, subprocess primitives, streaming
responses, struct/enum types, browser/JS codegen target. Codegen bug
list of 9 items tracked separately. The El sources here are runtime-
ready under the canonical C runtime; the gaps are language/runtime
extensions still in flight.
This commit is contained in:
Will Anderson
2026-04-30 18:18:39 -05:00
parent b2c22e6616
commit f3834267bf
20 changed files with 8534 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
// manifest.el el-ide-server vessel
vessel "el-ide-server" {
version "0.1.0"
description "Engram IDE backend: HTTP server, file ops, build/run, LSP bridge, plugin host, settings."
authors ["Will Anderson <will@neurontechnologies.ai>"]
edition "2026"
}
dependencies {
el-lsp "0.1"
el-plugin-host "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.el-ide-server-asan</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -0,0 +1,5 @@
---
triple: 'arm64-apple-darwin'
binary-path: el-ide-server-asan
relocations: []
...
+567
View File
@@ -0,0 +1,567 @@
// main.el el-ide-server
//
// HTTP backend for the Engram IDE. Pure-El port of the el-ide-server Rust
// crate. Wires the el-lsp and el-plugin-host vessels together and serves
// the IDE-frontend protocol used by the React-style frontend.
//
// El compilation note: `import` statements are parsed but the codegen
// concatenates nothing. The build script for this vessel concatenates
//
// vessels/el-lsp/src/main.el (without the trailing entry call)
// vessels/el-plugin-host/src/main.el
// vessels/el-ide-server/src/main.el (this file)
//
// into a single _combined.el before invoking elc. This file therefore
// assumes the lsp_* and plugin_* functions defined in those siblings are
// in scope. See README / build.sh.
//
// All routes serve JSON over HTTP. Query parameters are parsed by hand
// (the runtime gives us strings and a request body). Streaming endpoints
// (SSE) are emulated by returning the full output once the runtime's
// http_serve handler signature is response-as-string, so true server-sent
// streaming requires a runtime extension. Flagged in the report.
// Tiny utility layer
fn jstr(s: String) -> String {
let chars: [String] = native_string_chars(s)
let n: Int = native_list_len(chars)
let out: String = "\""
let i: Int = 0
while i < n {
let ch: String = native_list_get(chars, i)
if ch == "\"" { let out = out + "\\\"" }
else { if ch == "\\" { let out = out + "\\\\" }
else { if ch == "\n" { let out = out + "\\n" }
else { if ch == "\r" { let out = out + "\\r" }
else { if ch == "\t" { let out = out + "\\t" }
else { let out = out + ch } } } } }
let i = i + 1
}
out + "\""
}
fn jerr(msg: String) -> String {
"{\"error\":" + jstr(msg) + "}"
}
fn ok_obj() -> String {
"{\"ok\":true}"
}
// URL/query parsing
fn url_decode(s: String) -> String {
let chars: [String] = native_string_chars(s)
let n: Int = native_list_len(chars)
let out: String = ""
let i: Int = 0
while i < n {
let ch: String = native_list_get(chars, i)
if ch == "+" {
let out = out + " "
let i = i + 1
} else { if ch == "%" {
// Read two hex digits
if i + 2 < n {
let hex: String = native_list_get(chars, i + 1) + native_list_get(chars, i + 2)
let code: Int = hex_to_int(hex)
let out = out + char_from_code(code)
let i = i + 3
} else {
let out = out + ch
let i = i + 1
}
} else {
let out = out + ch
let i = i + 1
} }
}
out
}
fn hex_digit(ch: String) -> Int {
if ch == "0" { return 0 } if ch == "1" { return 1 }
if ch == "2" { return 2 } if ch == "3" { return 3 }
if ch == "4" { return 4 } if ch == "5" { return 5 }
if ch == "6" { return 6 } if ch == "7" { return 7 }
if ch == "8" { return 8 } if ch == "9" { return 9 }
if ch == "a" { return 10 } if ch == "A" { return 10 }
if ch == "b" { return 11 } if ch == "B" { return 11 }
if ch == "c" { return 12 } if ch == "C" { return 12 }
if ch == "d" { return 13 } if ch == "D" { return 13 }
if ch == "e" { return 14 } if ch == "E" { return 14 }
if ch == "f" { return 15 } if ch == "F" { return 15 }
-1
}
fn hex_to_int(hex: String) -> Int {
let chars: [String] = native_string_chars(hex)
let n: Int = native_list_len(chars)
let acc: Int = 0
let i: Int = 0
while i < n {
let d: Int = hex_digit(native_list_get(chars, i))
if d < 0 { return acc }
let acc = acc * 16 + d
let i = i + 1
}
acc
}
// Map an ASCII code point back to its single-byte string. Above 127 the
// runtime would need char_from_code(>127) multi-byte; we only need
// printable ASCII for query strings here.
fn char_from_code(code: Int) -> String {
let table: String = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
if code < 32 { return " " }
if code > 126 { return "?" }
str_slice(table, code - 32, code - 31)
}
fn split_path_query(path: String) -> Map<String, Any> {
let q: Int = str_index_of(path, "?")
if q < 0 { return { "path": path, "query": "" } }
{ "path": str_slice(path, 0, q), "query": str_slice(path, q + 1, str_len(path)) }
}
// query_get: pull the value of a single key out of `a=1&b=hello%20world`.
fn query_get(query: String, key: String) -> String {
if str_eq(query, "") { return "" }
let pairs: [String] = str_split(query, "&")
let n: Int = native_list_len(pairs)
let prefix: String = key + "="
let i: Int = 0
while i < n {
let pair: String = native_list_get(pairs, i)
if str_starts_with(pair, prefix) {
return url_decode(str_slice(pair, str_len(prefix), str_len(pair)))
}
if str_eq(pair, key) {
return ""
}
let i = i + 1
}
""
}
// Configuration
fn cfg_port() -> Int {
let p: String = env("EL_IDE_PORT")
if str_eq(p, "") { return 7771 }
str_to_int(p)
}
fn cfg_project_path() -> String {
let p: String = env("EL_IDE_PROJECT_PATH")
if str_eq(p, "") { return "." }
p
}
fn cfg_engram_url() -> String {
let u: String = env("EL_ENGRAM_URL")
if str_eq(u, "") { return "http://localhost:8742" }
u
}
fn cfg_el_binary() -> String {
let b: String = env("EL_BINARY")
if str_eq(b, "") { return "el" }
b
}
// Path safety
fn safe_join(rel: String) -> String {
let root: String = cfg_project_path()
// Reject obvious traversals.
if str_contains(rel, "..") { return "" }
if str_starts_with(rel, "/") { return "" }
if str_eq(rel, ".") { return root }
root + "/" + rel
}
// Status route
fn route_status() -> String {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "\"project_name\":" + jstr(cfg_project_path()))
let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path()))
let parts = native_list_append(parts, "\"version\":" + jstr("0.1.0"))
let parts = native_list_append(parts, "\"engine\":" + jstr("el-ide"))
"{" + list_join(parts, ",") + "}"
}
// Config route
fn route_config() -> String {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "\"port\":" + int_to_str(cfg_port()))
let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path()))
let parts = native_list_append(parts, "\"engram_url\":" + jstr(cfg_engram_url()))
let parts = native_list_append(parts, "\"el_binary\":" + jstr(cfg_el_binary()))
"{" + list_join(parts, ",") + "}"
}
// File API
fn route_files_list(query: String) -> String {
let rel: String = query_get(query, "path")
if str_eq(rel, "") { let rel = "." }
let target: String = safe_join(rel)
if str_eq(target, "") { return jerr("path escapes project root") }
let entries: [String] = fs_list(target)
let n: Int = native_list_len(entries)
let out: String = "["
let i: Int = 0
while i < n {
let name: String = native_list_get(entries, i)
// Skip hidden + noise dirs
let skip: Bool = false
if str_starts_with(name, ".") { let skip = true }
if str_eq(name, "target") { let skip = true }
if str_eq(name, "node_modules") { let skip = true }
if str_eq(name, "dist") { let skip = true }
if !skip {
if i > 0 {
if !str_eq(out, "[") { let out = out + "," }
}
// Approximate is_dir by checking for an extension
let dot: Int = str_index_of(name, ".")
let is_dir: String = "false"
if dot < 0 { let is_dir = "true" }
let out = out + "{\"name\":" + jstr(name) + ",\"path\":" + jstr(rel + "/" + name) + ",\"is_dir\":" + is_dir + ",\"children\":null}"
}
let i = i + 1
}
out + "]"
}
fn route_file_read(query: String) -> String {
let rel: String = query_get(query, "path")
if str_eq(rel, "") { return jerr("missing path parameter") }
let target: String = safe_join(rel)
if str_eq(target, "") { return jerr("path escapes project root") }
let content: String = fs_read(target)
"{\"path\":" + jstr(rel) + ",\"content\":" + jstr(content) + "}"
}
fn route_file_write(body: String) -> String {
let rel: String = json_get_string(body, "path")
let content: String = json_get_string(body, "content")
if str_eq(rel, "") { return jerr("missing path") }
let target: String = safe_join(rel)
if str_eq(target, "") { return jerr("path escapes project root") }
let ok: Bool = fs_write(target, content)
if ok { return ok_obj() }
jerr("cannot write file")
}
// LSP routes (delegate to inlined lsp_* functions)
fn route_lsp_complete(query: String) -> String {
let source: String = query_get(query, "source")
let pos: Int = str_to_int(query_get(query, "pos"))
lsp_complete(source, pos)
}
fn route_lsp_hover(query: String) -> String {
let source: String = query_get(query, "source")
let pos: Int = str_to_int(query_get(query, "pos"))
lsp_hover(source, pos)
}
fn route_lsp_errors(query: String) -> String {
let source: String = query_get(query, "source")
lsp_diagnostics(source)
}
fn route_lsp_jsonrpc(body: String) -> String {
let method: String = json_get_string(body, "method")
let params_raw: String = json_get_raw(body, "params")
let id_val: String = json_get_raw(body, "id")
if str_eq(id_val, "") { let id_val = "null" }
let result: String = lsp_jsonrpc(method, params_raw)
"{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}"
}
fn route_outline(query: String) -> String {
let source: String = query_get(query, "source")
lsp_outline(source)
}
fn route_definition(query: String) -> String {
let source: String = query_get(query, "source")
let pos: Int = str_to_int(query_get(query, "pos"))
lsp_definition(source, pos)
}
fn route_references(query: String) -> String {
let source: String = query_get(query, "source")
let pos: Int = str_to_int(query_get(query, "pos"))
lsp_references(source, pos)
}
fn route_format(body: String) -> String {
let content: String = json_get_string(body, "content")
lsp_format(content)
}
fn route_type_graph(query: String) -> String {
let source: String = query_get(query, "source")
lsp_type_graph(source)
}
// Themes
fn active_theme() -> String {
let t: String = state_get("ide/active_theme")
if str_eq(t, "") { return "dark" }
t
}
fn route_themes_list() -> String {
let active: String = active_theme()
let names: [String] = native_list_empty()
let names = native_list_append(names, "dark")
let names = native_list_append(names, "light")
let names = native_list_append(names, "neuron")
let names = native_list_append(names, "high-contrast")
let labels: [String] = native_list_empty()
let labels = native_list_append(labels, "Dark")
let labels = native_list_append(labels, "Light")
let labels = native_list_append(labels, "Neuron")
let labels = native_list_append(labels, "High Contrast")
let n: Int = native_list_len(names)
let out: String = "["
let i: Int = 0
while i < n {
if i > 0 { let out = out + "," }
let nm: String = native_list_get(names, i)
let lbl: String = native_list_get(labels, i)
let act: String = "false"
if str_eq(nm, active) { let act = "true" }
let out = out + "{\"name\":" + jstr(nm) + ",\"label\":" + jstr(lbl) + ",\"active\":" + act + "}"
let i = i + 1
}
out + "]"
}
fn route_themes_set(body: String) -> String {
let name: String = json_get_string(body, "name")
let valid: Bool = false
if str_eq(name, "dark") { let valid = true }
if str_eq(name, "light") { let valid = true }
if str_eq(name, "neuron") { let valid = true }
if str_eq(name, "high-contrast") { let valid = true }
if !valid { return jerr("unknown theme: " + name) }
state_set("ide/active_theme", name)
"{\"ok\":true,\"active\":" + jstr(name) + "}"
}
// Plugins (delegate to plugin_* in plugin-host)
fn route_plugins_list() -> String { plugin_list_json() }
fn route_plugins_install(body: String) -> String {
plugin_install(json_get_string(body, "name"))
}
fn route_plugins_remove(body: String) -> String {
plugin_remove(json_get_string(body, "name"))
}
fn route_plugins_enable(body: String) -> String {
plugin_enable(json_get_string(body, "name"))
}
fn route_plugins_disable(body: String) -> String {
plugin_disable(json_get_string(body, "name"))
}
// Snippets
fn route_snippets() -> String {
let snips: [String] = native_list_empty()
let snips = native_list_append(snips, "{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}")
let snips = native_list_append(snips, "{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}")
let snips = native_list_append(snips, "{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}")
"[" + list_join(snips, ",") + "]"
}
// Settings (file-backed)
fn settings_path() -> String {
let home: String = env("HOME")
if str_eq(home, "") { let home = "/tmp" }
home + "/.el-ide/settings.json"
}
fn default_settings_json() -> String {
"{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}"
}
fn route_settings_get() -> String {
let s: String = fs_read(settings_path())
if str_eq(s, "") { return default_settings_json() }
s
}
fn route_settings_save(body: String) -> String {
// Body is { "settings": {...} } extract the inner blob.
let blob: String = json_get_raw(body, "settings")
if str_eq(blob, "") { return jerr("missing settings") }
fs_write(settings_path(), blob)
blob
}
fn route_settings_reset() -> String {
fs_write(settings_path(), default_settings_json())
default_settings_json()
}
// Build / run (synchronous; no SSE in current http_serve runtime) ──────────
//
// The Rust crate spawned `el build/run` and streamed output as SSE events.
// El's `http_serve` is request/response only handlers return one string.
// We call the el binary, capture stdout via fs_read of a redirected temp
// file, and return JSON. Streaming is a runtime gap (see report).
fn route_build_or_run(action: String, body: String) -> String {
let rel_file: String = json_get_string(body, "file")
if str_eq(rel_file, "") { let rel_file = "src/main.el" }
let target: String = safe_join(rel_file)
if str_eq(target, "") { return jerr("path escapes project root") }
// No process-spawn primitive in the runtime; this is a stub that
// documents the planned behaviour. See report for runtime gap.
"{\"event\":\"info\",\"action\":" + jstr(action) + ",\"file\":" + jstr(rel_file) + ",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}"
}
fn route_build(body: String) -> String { route_build_or_run("build", body) }
fn route_run(body: String) -> String { route_build_or_run("run", body) }
// Engram reasoning proxy
fn route_reason(body: String) -> String {
let url: String = cfg_engram_url() + "/api/reason"
let resp: String = http_post(url, body)
if str_eq(resp, "") { return jerr("engram unreachable") }
resp
}
fn route_activate_preview(query: String) -> String {
let type_name: String = query_get(query, "type_name")
let q: String = query_get(query, "query")
let url: String = cfg_engram_url() + "/api/activate-preview"
let body: String = "{\"type_name\":" + jstr(type_name) + ",\"query\":" + jstr(q) + ",\"limit\":5}"
let resp: String = http_post(url, body)
if str_eq(resp, "") {
return "{\"count\":0,\"nodes\":[],\"connected\":false}"
}
resp
}
// Index page (minimal placeholder; the real bundle lives in ide/index.html) ─
fn route_index() -> String {
let path: String = cfg_project_path() + "/ide/index.html"
let html: String = fs_read(path)
if !str_eq(html, "") { return html }
"<!doctype html><html><head><title>el-ide</title></head><body><h1>el-ide</h1><p>Backend running. UI bundle missing at <code>" + path + "</code>.</p></body></html>"
}
// Health
fn route_health() -> String {
"{\"status\":\"ok\",\"engine\":\"el-ide-server\"}"
}
// Dispatcher
fn handle_request(method: String, path: String, body: String) -> String {
let parsed: Map<String, Any> = split_path_query(path)
let route: String = parsed["path"]
let query: String = parsed["query"]
if str_eq(route, "/") { return route_index() }
if str_eq(route, "/health") { return route_health() }
if str_eq(route, "/api/status") { return route_status() }
if str_eq(route, "/api/config") { return route_config() }
// Files
if str_eq(route, "/api/files") { return route_files_list(query) }
if str_eq(route, "/api/file") {
if str_eq(method, "GET") { return route_file_read(query) }
if str_eq(method, "POST") { return route_file_write(body) }
}
// Search minimal stub (real impl needs recursive grep)
if str_eq(route, "/api/search") {
let q: String = query_get(query, "q")
return "{\"query\":" + jstr(q) + ",\"results\":[]}"
}
// LSP
if str_eq(route, "/api/lsp/complete") { return route_lsp_complete(query) }
if str_eq(route, "/api/lsp/hover") { return route_lsp_hover(query) }
if str_eq(route, "/api/lsp/errors") { return route_lsp_errors(query) }
if str_eq(route, "/api/lsp/activate-preview") { return route_activate_preview(query) }
if str_eq(route, "/api/lsp/jsonrpc") { return route_lsp_jsonrpc(body) }
if str_eq(route, "/api/type-graph") { return route_type_graph(query) }
if str_eq(route, "/api/outline") { return route_outline(query) }
if str_eq(route, "/api/definition") { return route_definition(query) }
if str_eq(route, "/api/references") { return route_references(query) }
if str_eq(route, "/api/format") { return route_format(body) }
if str_eq(route, "/api/completions/snippet") { return route_snippets() }
// Themes
if str_eq(route, "/api/themes") { return route_themes_list() }
if str_eq(route, "/api/themes/active") { return route_themes_set(body) }
// Plugins
if str_eq(route, "/api/plugins") { return route_plugins_list() }
if str_eq(route, "/api/plugins/install") { return route_plugins_install(body) }
if str_eq(route, "/api/plugins/remove") { return route_plugins_remove(body) }
if str_eq(route, "/api/plugins/enable") { return route_plugins_enable(body) }
if str_eq(route, "/api/plugins/disable") { return route_plugins_disable(body) }
// Settings
if str_eq(route, "/api/settings") {
if str_eq(method, "GET") { return route_settings_get() }
if str_eq(method, "POST") { return route_settings_save(body) }
if str_eq(method, "DELETE") { return route_settings_reset() }
}
// Build / run
if str_eq(route, "/api/build") { return route_build(body) }
if str_eq(route, "/api/run") { return route_run(body) }
// Reasoning proxy
if str_eq(route, "/api/reason") { return route_reason(body) }
// Git relies on shell exec; runtime gap. Stub returns empty.
if str_eq(route, "/api/git/status") { return "[]" }
if str_eq(route, "/api/git/diff") { return "\"\"" }
"{\"error\":\"not found\",\"path\":" + jstr(route) + "}"
}
// Entry
fn server_main() -> Void {
plugin_init()
let port: Int = cfg_port()
println("[el-ide-server] project=" + cfg_project_path())
println("[el-ide-server] engram=" + cfg_engram_url())
println("[el-ide-server] listening on http://0.0.0.0:" + int_to_str(port))
http_set_handler("handle_request")
http_serve(port, "handle_request")
}
server_main()
+17
View File
@@ -0,0 +1,17 @@
// manifest.el el-lsp vessel
//
// Language Server core for the El language. Exposes hover, completion,
// diagnostics, outline, format, and type-graph entry points consumed by
// el-ide-server (HTTP) and stdio-mode editors.
vessel "el-lsp" {
version "0.1.0"
description "El language server: completion, hover, diagnostics, outline, format, type graph."
authors ["Will Anderson <will@neurontechnologies.ai>"]
edition "2026"
}
build {
entry "src/main.el"
output "dist/"
}
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
View File
+13
View File
@@ -0,0 +1,13 @@
// manifest.el el-plugin-host vessel
vessel "el-plugin-host" {
version "0.1.0"
description "Plugin lifecycle manager for el-ide. First-party plugin registry, install/remove/enable/disable."
authors ["Will Anderson <will@neurontechnologies.ai>"]
edition "2026"
}
build {
entry "src/main.el"
output "dist/"
}
Binary file not shown.
+311
View File
@@ -0,0 +1,311 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t pkey(el_val_t name, el_val_t field);
el_val_t put_str(el_val_t name, el_val_t field, el_val_t value);
el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value);
el_val_t get_str(el_val_t name, el_val_t field);
el_val_t get_bool(el_val_t name, el_val_t field);
el_val_t index_list(void);
el_val_t index_set(el_val_t names);
el_val_t index_contains(el_val_t name);
el_val_t index_add(el_val_t name);
el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party);
el_val_t plugin_init(void);
el_val_t json_escape_str(el_val_t s);
el_val_t quote_json(el_val_t s);
el_val_t hooks_to_json(el_val_t hooks_csv);
el_val_t plugin_to_json(el_val_t name);
el_val_t plugin_list_json(void);
el_val_t plugin_get_json(el_val_t name);
el_val_t plugin_install(el_val_t name);
el_val_t plugin_remove(el_val_t name);
el_val_t plugin_enable(el_val_t name);
el_val_t plugin_disable(el_val_t name);
el_val_t host_main(void);
el_val_t pkey(el_val_t name, el_val_t field) {
return el_str_concat(el_str_concat(el_str_concat(EL_STR("plugin/"), name), EL_STR("/")), field);
return 0;
}
el_val_t put_str(el_val_t name, el_val_t field, el_val_t value) {
state_set(pkey(name, field), value);
return 0;
}
el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value) {
if (value) {
state_set(pkey(name, field), EL_STR("true"));
}
if (!value) {
state_set(pkey(name, field), EL_STR("false"));
}
return 0;
}
el_val_t get_str(el_val_t name, el_val_t field) {
return state_get(pkey(name, field));
return 0;
}
el_val_t get_bool(el_val_t name, el_val_t field) {
return str_eq(state_get(pkey(name, field)), EL_STR("true"));
return 0;
}
el_val_t index_list(void) {
el_val_t raw = state_get(EL_STR("plugins/index"));
if (str_eq(raw, EL_STR(""))) {
return native_list_empty();
}
return str_split(raw, EL_STR(","));
return 0;
}
el_val_t index_set(el_val_t names) {
state_set(EL_STR("plugins/index"), list_join(names, EL_STR(",")));
return 0;
}
el_val_t index_contains(el_val_t name) {
el_val_t names = index_list();
el_val_t n = native_list_len(names);
el_val_t i = 0;
while (i < n) {
if (str_eq(native_list_get(names, i), name)) {
return 1;
}
i = (i + 1);
}
return 0;
return 0;
}
el_val_t index_add(el_val_t name) {
if (!index_contains(name)) {
el_val_t names = index_list();
names = native_list_append(names, name);
index_set(names);
}
return 0;
}
el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party) {
put_str(name, EL_STR("name"), name);
put_str(name, EL_STR("version"), version);
put_str(name, EL_STR("description"), description);
put_bool(name, EL_STR("installed"), installed);
put_bool(name, EL_STR("enabled"), enabled);
put_str(name, EL_STR("hooks"), hooks);
put_bool(name, EL_STR("first_party"), first_party);
index_add(name);
return 0;
}
el_val_t plugin_init(void) {
if (str_eq(state_get(EL_STR("plugins/initialised")), EL_STR("true"))) {
return 0;
}
register_plugin(EL_STR("el-theme-dark"), EL_STR("1.0.0"), EL_STR("Dark theme — the default Engram IDE theme."), 1, 1, EL_STR(""), 1);
register_plugin(EL_STR("el-theme-light"), EL_STR("1.0.0"), EL_STR("Light theme for high-ambient-light environments."), 0, 0, EL_STR(""), 1);
register_plugin(EL_STR("el-fmt"), EL_STR("0.1.0"), EL_STR("Code formatter — auto-formats .el files on save."), 1, 1, EL_STR("on_save"), 1);
register_plugin(EL_STR("el-doc"), EL_STR("0.1.0"), EL_STR("Documentation generator — produces HTML docs from type definitions."), 0, 0, EL_STR("custom_tool"), 1);
register_plugin(EL_STR("el-test"), EL_STR("0.1.0"), EL_STR("Test runner with inline results displayed in the editor gutter."), 0, 0, EL_STR("on_build_complete,custom_panel"), 1);
state_set(EL_STR("plugins/initialised"), EL_STR("true"));
return 0;
}
el_val_t json_escape_str(el_val_t s) {
el_val_t chars = native_string_chars(s);
el_val_t n = native_list_len(chars);
el_val_t out = EL_STR("");
el_val_t i = 0;
while (i < n) {
el_val_t ch = native_list_get(chars, i);
if (str_eq(ch, EL_STR("\""))) {
out = el_str_concat(out, EL_STR("\\\""));
} else {
if (str_eq(ch, EL_STR("\\"))) {
out = el_str_concat(out, EL_STR("\\\\"));
} else {
if (str_eq(ch, EL_STR("\n"))) {
out = el_str_concat(out, EL_STR("\\n"));
} else {
out = el_str_concat(out, ch);
}
}
}
i = (i + 1);
}
return out;
return 0;
}
el_val_t quote_json(el_val_t s) {
return el_str_concat(el_str_concat(EL_STR("\""), json_escape_str(s)), EL_STR("\""));
return 0;
}
el_val_t hooks_to_json(el_val_t hooks_csv) {
if (str_eq(hooks_csv, EL_STR(""))) {
return EL_STR("[]");
}
el_val_t xs = str_split(hooks_csv, EL_STR(","));
el_val_t n = native_list_len(xs);
el_val_t out = EL_STR("[");
el_val_t i = 0;
while (i < n) {
if (i > 0) {
out = el_str_concat(out, EL_STR(","));
}
out = el_str_concat(out, quote_json(native_list_get(xs, i)));
i = (i + 1);
}
return el_str_concat(out, EL_STR("]"));
return 0;
}
el_val_t plugin_to_json(el_val_t name) {
el_val_t parts = native_list_empty();
parts = native_list_append(parts, el_str_concat(EL_STR("\"name\":"), quote_json(get_str(name, EL_STR("name")))));
parts = native_list_append(parts, el_str_concat(EL_STR("\"version\":"), quote_json(get_str(name, EL_STR("version")))));
parts = native_list_append(parts, el_str_concat(EL_STR("\"description\":"), quote_json(get_str(name, EL_STR("description")))));
el_val_t installed = get_str(name, EL_STR("installed"));
el_val_t enabled = get_str(name, EL_STR("enabled"));
el_val_t first_party = get_str(name, EL_STR("first_party"));
parts = native_list_append(parts, el_str_concat(EL_STR("\"installed\":"), installed));
parts = native_list_append(parts, el_str_concat(EL_STR("\"enabled\":"), enabled));
parts = native_list_append(parts, el_str_concat(EL_STR("\"hooks\":"), hooks_to_json(get_str(name, EL_STR("hooks")))));
parts = native_list_append(parts, el_str_concat(EL_STR("\"first_party\":"), first_party));
return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"));
return 0;
}
el_val_t plugin_list_json(void) {
plugin_init();
el_val_t names = index_list();
el_val_t n = native_list_len(names);
el_val_t out = EL_STR("[");
el_val_t i = 0;
while (i < n) {
if (i > 0) {
out = el_str_concat(out, EL_STR(","));
}
out = el_str_concat(out, plugin_to_json(native_list_get(names, i)));
i = (i + 1);
}
return el_str_concat(out, EL_STR("]"));
return 0;
}
el_val_t plugin_get_json(el_val_t name) {
plugin_init();
if (!index_contains(name)) {
return EL_STR("null");
}
return plugin_to_json(name);
return 0;
}
el_val_t plugin_install(el_val_t name) {
plugin_init();
if (!index_contains(name)) {
return EL_STR("{\"error\":\"plugin registry is not available (registry URL not configured)\"}");
}
if (get_bool(name, EL_STR("installed"))) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' is already installed\"}"));
}
put_bool(name, EL_STR("installed"), 1);
put_bool(name, EL_STR("enabled"), 1);
return plugin_to_json(name);
return 0;
}
el_val_t plugin_remove(el_val_t name) {
plugin_init();
if (!index_contains(name)) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}"));
}
if (str_eq(name, EL_STR("el-theme-dark"))) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' cannot be removed: it is a required built-in\"}"));
}
put_bool(name, EL_STR("installed"), 0);
put_bool(name, EL_STR("enabled"), 0);
return EL_STR("{\"ok\":true}");
return 0;
}
el_val_t plugin_enable(el_val_t name) {
plugin_init();
if (!index_contains(name)) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}"));
}
put_bool(name, EL_STR("enabled"), 1);
return EL_STR("{\"ok\":true}");
return 0;
}
el_val_t plugin_disable(el_val_t name) {
plugin_init();
if (!index_contains(name)) {
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}"));
}
put_bool(name, EL_STR("enabled"), 0);
return EL_STR("{\"ok\":true}");
return 0;
}
el_val_t host_main(void) {
el_val_t argv = args();
el_val_t n = native_list_len(argv);
if (n == 0) {
println(plugin_list_json());
return 0;
}
el_val_t cmd = native_list_get(argv, 0);
if (str_eq(cmd, EL_STR("list"))) {
println(plugin_list_json());
return 0;
}
if (str_eq(cmd, EL_STR("get"))) {
if (n >= 2) {
println(plugin_get_json(native_list_get(argv, 1)));
}
return 0;
}
if (str_eq(cmd, EL_STR("install"))) {
if (n >= 2) {
println(plugin_install(native_list_get(argv, 1)));
}
return 0;
}
if (str_eq(cmd, EL_STR("remove"))) {
if (n >= 2) {
println(plugin_remove(native_list_get(argv, 1)));
}
return 0;
}
if (str_eq(cmd, EL_STR("enable"))) {
if (n >= 2) {
println(plugin_enable(native_list_get(argv, 1)));
}
return 0;
}
if (str_eq(cmd, EL_STR("disable"))) {
if (n >= 2) {
println(plugin_disable(native_list_get(argv, 1)));
}
return 0;
}
println(EL_STR("usage: el-plugin-host [list|get|install|remove|enable|disable] <name>"));
return 0;
}
int main(int argc, char** argv) {
el_runtime_init_args(argc, argv);
host_main();
return 0;
}
+251
View File
@@ -0,0 +1,251 @@
// main.el el-plugin-host
//
// Plugin lifecycle manager for el-ide. Pure-El port of the el-plugin-host
// Rust crate.
//
// Plugins are stored in process-global state via state_get / state_set.
// Each plugin lives under a key prefix `plugin/<name>/<field>`. The
// canonical list of plugin names is maintained at `plugins/index` as a
// comma-separated string (we don't have list serialisation in state yet).
//
// Public API:
// plugin_init() seed first-party plugins (idempotent)
// plugin_list_json() -> String JSON array of all plugins
// plugin_get_json(name) -> String JSON object or "null"
// plugin_install(name) -> String JSON { ok | error }
// plugin_remove(name) -> String
// plugin_enable(name) -> String
// plugin_disable(name) -> String
// State helpers (string K/V)
fn pkey(name: String, field: String) -> String {
"plugin/" + name + "/" + field
}
fn put_str(name: String, field: String, value: String) -> Void {
state_set(pkey(name, field), value)
}
fn put_bool(name: String, field: String, value: Bool) -> Void {
if value { state_set(pkey(name, field), "true") }
if !value { state_set(pkey(name, field), "false") }
}
fn get_str(name: String, field: String) -> String {
state_get(pkey(name, field))
}
fn get_bool(name: String, field: String) -> Bool {
str_eq(state_get(pkey(name, field)), "true")
}
// Plugin index: comma-separated list of names at "plugins/index".
fn index_list() -> [String] {
let raw: String = state_get("plugins/index")
if str_eq(raw, "") { return native_list_empty() }
str_split(raw, ",")
}
fn index_set(names: [String]) -> Void {
state_set("plugins/index", list_join(names, ","))
}
fn index_contains(name: String) -> Bool {
let names: [String] = index_list()
let n: Int = native_list_len(names)
let i: Int = 0
while i < n {
if str_eq(native_list_get(names, i), name) { return true }
let i = i + 1
}
false
}
fn index_add(name: String) -> Void {
if !index_contains(name) {
let names: [String] = index_list()
let names = native_list_append(names, name)
index_set(names)
}
}
// Initialisation
fn register_plugin(name: String, version: String, description: String,
installed: Bool, enabled: Bool, hooks: String,
first_party: Bool) -> Void {
put_str(name, "name", name)
put_str(name, "version", version)
put_str(name, "description", description)
put_bool(name, "installed", installed)
put_bool(name, "enabled", enabled)
put_str(name, "hooks", hooks)
put_bool(name, "first_party", first_party)
index_add(name)
}
fn plugin_init() -> Void {
if str_eq(state_get("plugins/initialised"), "true") {
return
}
register_plugin("el-theme-dark", "1.0.0", "Dark theme — the default Engram IDE theme.", true, true, "", true)
register_plugin("el-theme-light", "1.0.0", "Light theme for high-ambient-light environments.", false, false, "", true)
register_plugin("el-fmt", "0.1.0", "Code formatter — auto-formats .el files on save.", true, true, "on_save", true)
register_plugin("el-doc", "0.1.0", "Documentation generator — produces HTML docs from type definitions.", false, false, "custom_tool", true)
register_plugin("el-test", "0.1.0", "Test runner with inline results displayed in the editor gutter.", false, false, "on_build_complete,custom_panel", true)
state_set("plugins/initialised", "true")
}
// Serialisation
fn json_escape_str(s: String) -> String {
let chars: [String] = native_string_chars(s)
let n: Int = native_list_len(chars)
let out: String = ""
let i: Int = 0
while i < n {
let ch: String = native_list_get(chars, i)
if ch == "\"" { let out = out + "\\\"" }
else { if ch == "\\" { let out = out + "\\\\" }
else { if ch == "\n" { let out = out + "\\n" }
else { let out = out + ch } } }
let i = i + 1
}
out
}
fn quote_json(s: String) -> String {
"\"" + json_escape_str(s) + "\""
}
fn hooks_to_json(hooks_csv: String) -> String {
if str_eq(hooks_csv, "") { return "[]" }
let xs: [String] = str_split(hooks_csv, ",")
let n: Int = native_list_len(xs)
let out: String = "["
let i: Int = 0
while i < n {
if i > 0 { let out = out + "," }
let out = out + quote_json(native_list_get(xs, i))
let i = i + 1
}
out + "]"
}
fn plugin_to_json(name: String) -> String {
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "\"name\":" + quote_json(get_str(name, "name")))
let parts = native_list_append(parts, "\"version\":" + quote_json(get_str(name, "version")))
let parts = native_list_append(parts, "\"description\":" + quote_json(get_str(name, "description")))
let installed: String = get_str(name, "installed")
let enabled: String = get_str(name, "enabled")
let first_party: String = get_str(name, "first_party")
let parts = native_list_append(parts, "\"installed\":" + installed)
let parts = native_list_append(parts, "\"enabled\":" + enabled)
let parts = native_list_append(parts, "\"hooks\":" + hooks_to_json(get_str(name, "hooks")))
let parts = native_list_append(parts, "\"first_party\":" + first_party)
"{" + list_join(parts, ",") + "}"
}
fn plugin_list_json() -> String {
plugin_init()
let names: [String] = index_list()
let n: Int = native_list_len(names)
let out: String = "["
let i: Int = 0
while i < n {
if i > 0 { let out = out + "," }
let out = out + plugin_to_json(native_list_get(names, i))
let i = i + 1
}
out + "]"
}
fn plugin_get_json(name: String) -> String {
plugin_init()
if !index_contains(name) { return "null" }
plugin_to_json(name)
}
// Mutations
fn plugin_install(name: String) -> String {
plugin_init()
if !index_contains(name) {
return "{\"error\":\"plugin registry is not available (registry URL not configured)\"}"
}
if get_bool(name, "installed") {
return "{\"error\":\"plugin '" + json_escape_str(name) + "' is already installed\"}"
}
put_bool(name, "installed", true)
put_bool(name, "enabled", true)
plugin_to_json(name)
}
fn plugin_remove(name: String) -> String {
plugin_init()
if !index_contains(name) {
return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
}
if str_eq(name, "el-theme-dark") {
return "{\"error\":\"plugin '" + json_escape_str(name) + "' cannot be removed: it is a required built-in\"}"
}
put_bool(name, "installed", false)
put_bool(name, "enabled", false)
"{\"ok\":true}"
}
fn plugin_enable(name: String) -> String {
plugin_init()
if !index_contains(name) {
return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
}
put_bool(name, "enabled", true)
"{\"ok\":true}"
}
fn plugin_disable(name: String) -> String {
plugin_init()
if !index_contains(name) {
return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
}
put_bool(name, "enabled", false)
"{\"ok\":true}"
}
// Standalone CLI
fn host_main() -> Void {
let argv: [String] = args()
let n: Int = native_list_len(argv)
if n == 0 {
println(plugin_list_json())
return
}
let cmd: String = native_list_get(argv, 0)
if str_eq(cmd, "list") { println(plugin_list_json()) return }
if str_eq(cmd, "get") {
if n >= 2 { println(plugin_get_json(native_list_get(argv, 1))) }
return
}
if str_eq(cmd, "install") {
if n >= 2 { println(plugin_install(native_list_get(argv, 1))) }
return
}
if str_eq(cmd, "remove") {
if n >= 2 { println(plugin_remove(native_list_get(argv, 1))) }
return
}
if str_eq(cmd, "enable") {
if n >= 2 { println(plugin_enable(native_list_get(argv, 1))) }
return
}
if str_eq(cmd, "disable") {
if n >= 2 { println(plugin_disable(native_list_get(argv, 1))) }
return
}
println("usage: el-plugin-host [list|get|install|remove|enable|disable] <name>")
}
host_main()
View File