Archived
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:
@@ -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
Executable
BIN
Binary file not shown.
Executable
BIN
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>
|
||||
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
triple: 'arm64-apple-darwin'
|
||||
binary-path: el-ide-server-asan
|
||||
relocations: []
|
||||
...
|
||||
@@ -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()
|
||||
@@ -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/"
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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/"
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user