@kexhq/kex 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @kexhq/kex
2
+
3
+ The [Kex](https://github.com/kexhq/kex) language interpreter compiled to
4
+ WebAssembly, packaged for embedding a Kex REPL in a web page or a Node
5
+ script. This is the same wasm binary built and tested in the main repo
6
+ (`src/wasm_repl.cxx`, `web/index.html`).
7
+
8
+ Published to the public npm registry as [`@kexhq/kex`](https://www.npmjs.com/package/@kexhq/kex),
9
+ and mirrored to GitHub Packages for internal consumers already wired to that
10
+ path (e.g. kex.run's site repo).
11
+
12
+ ## Status
13
+
14
+ **Pre-release.** The package's version follows the language's `VERSION` file:
15
+
16
+ - Every push to the repo publishes `<VERSION>-dev.<sha>` under the `next`
17
+ dist-tag (e.g. `0.3.0-beta.dev.a1b2c3d`) — a dev build names the version it
18
+ is working toward, distinguished by revision, exactly like a native
19
+ `kex --version` built from the same tree.
20
+ - A release publishes the plain `VERSION` (`0.3.0-beta`) with the same
21
+ dist-tag rule as the container images: `latest` for a stable release, the
22
+ channel name (`rc`, `beta`) for a pre-release. The `version` field in
23
+ `package.json` is a placeholder — the number is stamped at publish time.
24
+
25
+ Expect breaking changes without notice until 1.0.0; pin a version if you need
26
+ reproducible builds.
27
+
28
+ **Known limitation:** none currently — the Asyncify/JS-interop bug that
29
+ used to cause duplicated output and lost state for `receive`,
30
+ `receive timeout:`, and `Task.await` is fixed.
31
+
32
+ ## Installing
33
+
34
+ ```
35
+ npm install @kexhq/kex@next
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```js
41
+ import { Kex } from "@kexhq/kex";
42
+
43
+ const session = await Kex.create();
44
+
45
+ // Evaluate one chunk of Kex source at a time — state (let/var bindings,
46
+ // spawned processes) persists across calls on the same session, exactly
47
+ // like a real REPL.
48
+ console.log(await session.eval("1 + 2"));
49
+ // => "=> 3 : Int\n"
50
+
51
+ await session.eval("let x = 5");
52
+ console.log(await session.eval("x + 10"));
53
+ // => "=> 15 : Int\n"
54
+
55
+ // Tab completion — see web/index.html in the main repo for a full
56
+ // reference client (word-break-character scanning, history, line editing).
57
+ console.log(session.complete("IO.printL", 0, "IO.printL"));
58
+ // => ["IO.printLine"]
59
+
60
+ session.destroy();
61
+ ```
62
+
63
+ Ships with TypeScript definitions (`Kex`, `KexModuleOptions`) — no `@types`
64
+ package needed, and no `new Kex(...)`: the constructor is private, use
65
+ `Kex.create()`.
66
+
67
+ Multi-line input (`do ... end` blocks) needs to be accumulated into one
68
+ string before calling `eval` — this package doesn't do that for you (see
69
+ `web/index.html`'s `countBlocks()` for the exact logic the real REPL uses to
70
+ decide when a block is complete).
71
+
72
+ Output already contains ANSI color escape codes (matching the native CLI's
73
+ REPL exactly) — render it through a real terminal emulator (e.g.
74
+ [xterm.js](https://xtermjs.org/), as `web/index.html` does) rather than
75
+ stripping them, unless you specifically want plain text.
76
+
77
+ ## For kex.run specifically
78
+
79
+ This is the package kex.run is expected to import as its in-browser
80
+ interpreter. Float on `next` for development; for a deployed build pin an
81
+ exact version — a released one (`@kexhq/kex@0.3.0-beta`) or a specific dev
82
+ build (`@kexhq/kex@0.3.0-beta.dev.a1b2c3d`) — since `next` moves on every
83
+ push.
84
+
85
+ ## Building locally
86
+
87
+ From the main repo, with `emsdk` active (pinned to 5.0.7 — see
88
+ `third_party/gmp-wasm/README.md`):
89
+
90
+ ```
91
+ cd packages/kex
92
+ npm run prepack # builds build-wasm/ if needed, assembles dist/
93
+ npm pack # produces a .tgz you can npm install locally to test
94
+ ```
@@ -0,0 +1,53 @@
1
+ // Type definitions for @kexhq/kex
2
+
3
+ /** Extra options forwarded to the underlying Emscripten module factory. */
4
+ export interface KexModuleOptions {
5
+ locateFile?: (path: string, prefix: string) => string;
6
+ [key: string]: unknown;
7
+ }
8
+
9
+ /**
10
+ * A Kex REPL session backed by the wasm interpreter. Internal state (the
11
+ * Emscripten module and the C-side session handle) is fully private —
12
+ * construct instances via Kex.create(), not `new Kex(...)`.
13
+ */
14
+ export class Kex {
15
+ private constructor(module: unknown, session: number);
16
+
17
+ /** Creates a new, independent Kex REPL session backed by the wasm interpreter. */
18
+ static create(moduleOptions?: KexModuleOptions): Promise<Kex>;
19
+
20
+ /**
21
+ * Evaluates one chunk of Kex source against this session's persistent
22
+ * interpreter state. State (`let`/`var` bindings, spawned processes)
23
+ * persists across calls on the same session, like a real REPL.
24
+ *
25
+ * Multi-line `do ... end` blocks must be accumulated into one string by
26
+ * the caller before calling eval — see web/index.html in the main repo
27
+ * for the exact `countBlocks()` logic the real REPL uses.
28
+ *
29
+ * The returned string already contains ANSI color escape codes (matching
30
+ * the native CLI's REPL exactly) — render it through a real terminal
31
+ * emulator (e.g. xterm.js) rather than stripping them, unless you
32
+ * specifically want plain text.
33
+ */
34
+ eval(source: string): Promise<string>;
35
+
36
+ /**
37
+ * Tab completion, reusing the same logic the native REPL's readline
38
+ * integration uses. `line` is the current input line, `start` is the
39
+ * index where the word under the cursor begins, `text` is that word
40
+ * itself. Returns the list of completions (already rewritten to what
41
+ * should be inserted in place of `text`).
42
+ */
43
+ complete(line: string, start: number, text: string): string[];
44
+
45
+ /** Frees this session's underlying interpreter state. Safe to call more than once. */
46
+ destroy(): void;
47
+
48
+ /** The REPL banner with version info, matching the native CLI exactly. */
49
+ banner(): string;
50
+
51
+ /** Kex version string (e.g. "0.2.0"). */
52
+ version(): string;
53
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // Thin wrapper around the wasm REPL bindings (src/wasm_repl.cxx in the main
2
+ // repo) for consumption as an npm package — this is what kex.run imports to
3
+ // embed the interpreter. At publish time, the "prepack" build step (see
4
+ // ../README.md) copies this file into dist/ alongside the built
5
+ // kex_repl_wasm.js/.wasm/.data, so everything resolves relative to this same
6
+ // directory at runtime (works in both Node and a bundler, since it's plain
7
+ // import.meta.url-relative resolution, no cwd-dependent paths).
8
+ import createKexReplModule from "./kex_repl_wasm.js";
9
+
10
+ // Private fields keep the raw Emscripten Module and the C-side session
11
+ // pointer out of the public API entirely — callers only ever see eval/
12
+ // complete/destroy, never the ccall plumbing underneath.
13
+ export class Kex {
14
+ #module;
15
+ #session;
16
+ #destroyed = false;
17
+
18
+ // Not called directly — use Kex.create(), since standing up the wasm
19
+ // module is inherently async and a constructor can't await.
20
+ constructor(module, session) {
21
+ this.#module = module;
22
+ this.#session = session;
23
+ }
24
+
25
+ /** Creates a new, independent Kex REPL session backed by the wasm interpreter. */
26
+ static async create(moduleOptions = {}) {
27
+ const module = await createKexReplModule({
28
+ // In Node, the preload-file package (kex_repl_wasm.data) is fetched
29
+ // via a plain fs.readFileSync(path) call, which doesn't accept a
30
+ // file:// URL string — only a real filesystem path. url.pathname
31
+ // (not .href) gives that directly on POSIX. In a browser this needs
32
+ // an actual URL for fetch(), hence .href there instead.
33
+ locateFile: (path) => {
34
+ const url = new URL(path, import.meta.url);
35
+ return typeof window === "undefined" ? url.pathname : url.href;
36
+ },
37
+ ...moduleOptions,
38
+ });
39
+ const session = module.ccall("kex_repl_create", "number", [], []);
40
+ return new Kex(module, session);
41
+ }
42
+
43
+ #assertAlive() {
44
+ if (this.#destroyed) throw new Error("this Kex session has already been destroyed");
45
+ }
46
+
47
+ /**
48
+ * Evaluates one chunk of Kex source against this session's persistent
49
+ * interpreter state. State (`let`/`var` bindings, spawned processes)
50
+ * persists across calls on the same session, like a real REPL.
51
+ *
52
+ * Multi-line `do ... end` blocks must be accumulated into one string by
53
+ * the caller before calling eval — see web/index.html in the main repo
54
+ * for the exact `countBlocks()` logic the real REPL uses.
55
+ *
56
+ * The returned string already contains ANSI color escape codes (matching
57
+ * the native CLI's REPL exactly) — render it through a real terminal
58
+ * emulator (e.g. xterm.js) rather than stripping them, unless you
59
+ * specifically want plain text.
60
+ */
61
+ async eval(source) {
62
+ this.#assertAlive();
63
+ // kex_repl_eval is void-returning on purpose — see src/wasm_repl.cxx's
64
+ // own doc comment: a direct return value from an Asyncify-instrumented
65
+ // export wasn't reliably reaching the JS caller through ccall's async
66
+ // plumbing, so the result is fetched via a second, ordinary synchronous
67
+ // call instead (kex_repl_last_result never touches Asyncify).
68
+ await this.#module.ccall(
69
+ "kex_repl_eval", null,
70
+ ["number", "string"], [this.#session, source],
71
+ { async: true }
72
+ );
73
+ return this.#module.ccall("kex_repl_last_result", "string", ["number"], [this.#session]);
74
+ }
75
+
76
+ /**
77
+ * Tab completion, reusing the same logic the native REPL's readline
78
+ * integration uses. `line` is the current input line, `start` is the
79
+ * index where the word under the cursor begins, `text` is that word
80
+ * itself. Returns the list of completions (already rewritten to what
81
+ * should be inserted in place of `text`).
82
+ */
83
+ complete(line, start, text) {
84
+ this.#assertAlive();
85
+ const raw = this.#module.ccall(
86
+ "kex_repl_complete", "string",
87
+ ["number", "string", "number", "string"],
88
+ [this.#session, line, start, text]
89
+ );
90
+ return raw.split("\n").filter((m) => m.length > 0);
91
+ }
92
+
93
+ /** Frees this session's underlying interpreter state. Safe to call more than once. */
94
+ destroy() {
95
+ if (this.#destroyed) return;
96
+ this.#module.ccall("kex_repl_destroy", null, ["number"], [this.#session]);
97
+ this.#destroyed = true;
98
+ }
99
+
100
+ /** The REPL banner with version info, matching the native CLI exactly. */
101
+ banner() {
102
+ return this.#module.ccall("kex_repl_banner", "string", [], []);
103
+ }
104
+
105
+ /** Kex version string (e.g. "0.2.0"). */
106
+ version() {
107
+ return this.#module.ccall("kex_version", "string", [], []);
108
+ }
109
+ }