diff --git a/help.txt b/help.txt new file mode 100644 index 0000000..7158c7e --- /dev/null +++ b/help.txt @@ -0,0 +1,13 @@ +Ludus CLI (v0.1.0) + +The Ludus CLI has five modes: `help`, `version`, `run`, `listen`, and `send`. + +* `help`: Output this text and exit. + +* `version`: Ouput the current version number. + +* `run`: Runs a Ludus script: `ludus run {script}`. + +* `listen`: Starts a Ludus "listener" server, which receives Ludus source code on a given port and outputs the results. Similar to a REPL, intended to be used in conjunction with `send`, from a text editor. + +* `send`: Sends text from `stdin` to a listening server. Any text editor that can send highlighted text to a command line program can evaluate Ludus source. diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..0a7daf5 --- /dev/null +++ b/index.ts @@ -0,0 +1,19 @@ +import { listen } from "./listener" +import { run_file } from "./runner" +import { send } from "./sender" + +const help_text = await Bun.file("./help.txt").text() + +export async function main() { + const cmd = Bun.argv[2] + switch (cmd) { + case "listen": { return listen() } + case "send": { return send() } + case "run": { return run_file(Bun.argv[3]) } + case "help": { return console.log(help_text) } + case "version": { return console.log("0.1.0") } + default: console.log("Usage: ludus {help | version | run | listen | send}") + } +} + +await main() diff --git a/listener.ts b/listener.ts index ce342be..566052f 100644 --- a/listener.ts +++ b/listener.ts @@ -1,29 +1,38 @@ import { run } from "@ludus/ludus-js-pure" +import { unlinkSync, readFileSync } from "node:fs" -async function listen() { +export async function listen () { const port = Math.floor((Math.random() * 1000) + 50000) - - const repl_info = { port } - - const repl_file = Bun.file("./.lrepl") - + const session_token = crypto.randomUUID(); + const repl_file_path = ".lrepl" + const repl_info = { port, session: session_token } + const repl_file = Bun.file(repl_file_path) await Bun.write(repl_file, JSON.stringify(repl_info)) console.log(`Ludus REPL listening on localhost:${port}`) + console.log(`Session token: ${session_token}`) Bun.listen({ hostname: "localhost", port, socket: { - data: (socket, data) => { - console.log("I got data!") - const source = data.toString("utf-8") - const result = run(source) - const msgs = result.console - for (const msg of msgs) { - console.log(msg) + data: (_, data) => { + const payload = data.toString("utf-8") + let session, source + try { + const json_msg = JSON.parse(payload) + session = json_msg.session + source = json_msg.source + } catch (e) { + console.log("Received malformed message.") + return } - if (!result.errors) console.log(result.result) + if (session !== session_token) { + console.log("Received message not from this REPL session.") + return + } + const result = run(source) + if (!result.errors) console.log("=>", result.result) else { console.log("Ludus had some errors:") console.log(result.errors) @@ -32,9 +41,33 @@ async function listen() { error: (socket, error) => { console.log(error); socket.end() } } }) + + + process.on("exit", async function () { + try { cleanup(repl_file_path, session_token) } catch (e) {} + console.log("\nGoodbye.") + }) + + process.on("SIGINT", () => process.exit()) + + // Below here, I'm trying to get ctrl-d to work to close the listener + // the while (true) loop locks the listener + // see: https://github.com/oven-sh/bun/issues/3255 + // this should be doable with process.stdin.on("close"...) + //const mystdin = Bun.file("/dev/stdin").stream().getReader() + + // while (true) { + // const {done, value} = await mystdin.read() + // if (done) process.exit() + // console.log(value) + // } + } -await listen() - - +// Don't delete an .lrepl we don't own +function cleanup (path: string, session_token: string) { + const repl_file = readFileSync(path).toString("utf-8") + const {session} = JSON.parse(repl_file) + if (session === session_token) unlinkSync(path) +} diff --git a/package.json b/package.json index 4f8cd74..1bc82cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { - "name": "ludus-repl", + "name": "@ludus/cli", + "version": "0.1.0", + "description": "A CLI interface to Ludus.", + "main": "index.ts", "module": "index.ts", "type": "module", + "license": "GPL-3.0", "devDependencies": { "@types/bun": "latest" }, diff --git a/runner.ts b/runner.ts index b9861b1..9abce73 100644 --- a/runner.ts +++ b/runner.ts @@ -1,13 +1,20 @@ -import {run} from "@ludus/ludus-js-pure" +import { run } from "@ludus/ludus-js-pure" -const script = Bun.argv[2] -const file = Bun.file(script) -const source = await file.text() -console.log(`Source to run: -${source}`) - -const result = run(source) - -if (!result.errors) console.log(result.result) -else console.log(result.errors) +export async function run_file (path: string): Promise { + const handle = Bun.file(path) + const file_exists = await handle.exists() + if (!file_exists) { + console.log(`File not found: ${path}`) + return + } + const source = await handle.text() + const output = run(source) + if (output.errors) { + for (const error of output.errors) { + console.log(error) + } + } else { + console.log(output.result) + } +} diff --git a/sender.ts b/sender.ts index 5ef926e..b0b954b 100644 --- a/sender.ts +++ b/sender.ts @@ -1,19 +1,31 @@ -const repl_file = Bun.file("./.lrepl") -const repl_info = await repl_file.json() -const port = repl_info.port - -const socket = await Bun.connect({ - hostname: "localhost", - port, - socket: { - data: (_, data) => process.exit() +export async function send() { + const repl_file = Bun.file("./.lrepl") + let repl_info + try { + repl_info = await repl_file.json() + } catch (e) { + console.log("No .lrepl file found.") + process.exit(1) } -}) + const port = repl_info.port + const session = repl_info.session -for await (const input of Bun.stdin.stream()) { - const chunk = Buffer.from(input).toString("utf-8") - socket.write(chunk) - socket.end() - process.exit() + const socket = await Bun.connect({ + hostname: "localhost", + port, + socket: { + data: () => process.exit() + } + }) + + for await (const input of Bun.stdin.stream()) { + const chunk = Buffer.from(input).toString("utf-8") + const payload = {source: chunk, session} + // console.log("Sending:", payload) + // console.log(`To: ${port}`) + socket.write(JSON.stringify(payload)) + socket.end() + process.exit() + } }