|
| 1 | +// The moshcode shell — run `moshcode` with no args. A metal prompt that opens |
| 2 | +// passthrough sessions on any engine via `/agents <engine>`, installs engines, |
| 3 | +// and runs moshscript. Each session hands the whole terminal to the engine's own |
| 4 | +// CLI and takes it back on exit. |
| 5 | +import readline from "node:readline"; |
| 6 | +import { spawn } from "node:child_process"; |
| 7 | +import fs from "node:fs"; |
| 8 | +import { ENGINES, resolveEngine, engineStatus, openSession } from "./engines.mjs"; |
| 9 | +import { compile, run } from "./interpreter.mjs"; |
| 10 | +import { defaultCommands } from "./commands.mjs"; |
| 11 | +import { banner, hr, acid, ash, bone, dim, ok, err, info } from "./ui.mjs"; |
| 12 | + |
| 13 | +const PROMPT = () => acid("mosh ") + dim("▸ "); |
| 14 | +const mkrl = () => readline.createInterface({ input: process.stdin, output: process.stdout }); |
| 15 | +const ask = (rl) => new Promise((res) => rl.question(PROMPT(), res)); |
| 16 | + |
| 17 | +function printEngines() { |
| 18 | + console.log(bone(" engines") + ash(" — open one with ") + acid("/agents <name>")); |
| 19 | + for (const e of engineStatus()) { |
| 20 | + const dot = e.installed ? acid("●") : ash("○"); |
| 21 | + console.log(` ${dot} ${bone(e.key.padEnd(9))} ${ash(e.installed ? "installed" : "not installed — /install " + e.key)}`); |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +function printHelp() { |
| 26 | + console.log([ |
| 27 | + bone(" commands"), |
| 28 | + ` ${acid("/agents")} list coding engines`, |
| 29 | + ` ${acid("/agents <name>")} open a session (claude · codex · gemini · aider · opencode)`, |
| 30 | + ` ${acid("/install <name>")} install an engine`, |
| 31 | + ` ${acid("/run <file.mosh>")} run a moshscript program`, |
| 32 | + ` ${acid("/help")} this`, |
| 33 | + ` ${acid("/quit")} leave the pit (or Ctrl-D)`, |
| 34 | + "", |
| 35 | + ash(" shortcut: type an engine name by itself, e.g. ") + acid("claude"), |
| 36 | + ].join("\n")); |
| 37 | +} |
| 38 | + |
| 39 | +async function openEngine(key, engine, args) { |
| 40 | + if (!engine.installed && !args.length) { |
| 41 | + console.log(info(`${key} isn't installed — try ${acid("/install " + key)} first.`)); |
| 42 | + } |
| 43 | + console.log(info(`opening ${bone(key)} — hand-off to its CLI, exit it to come back…`)); |
| 44 | + console.log(hr()); |
| 45 | + const r = await openSession(engine, args); |
| 46 | + console.log(hr()); |
| 47 | + if (!r.ok) { |
| 48 | + console.log(r.error?.code === "ENOENT" |
| 49 | + ? err(`${key} isn't on PATH (\`${engine.bin}\`). install it with /install ${key}`) |
| 50 | + : err(`couldn't launch ${key}: ${r.error?.message || r.error}`)); |
| 51 | + } else { |
| 52 | + console.log(info(`${key} exited${r.code != null ? ` (code ${r.code})` : ""}. back in the pit.`)); |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +function installEngine(key) { |
| 57 | + return new Promise((resolve) => { |
| 58 | + const engine = ENGINES[key]; |
| 59 | + if (!engine) { console.log(err(`unknown engine "${key}"`)); return resolve(); } |
| 60 | + console.log(info(`installing ${key}: ${engine.install.cmd} ${engine.install.args.join(" ")}`)); |
| 61 | + console.log(hr()); |
| 62 | + const child = spawn(engine.install.cmd, engine.install.args, { stdio: "inherit" }); |
| 63 | + child.on("error", (e) => { console.log(hr()); console.log(err(`install failed: ${e.message}`)); resolve(); }); |
| 64 | + child.on("exit", (code) => { console.log(hr()); console.log(code === 0 ? ok(`${key} installed. 🤘`) : err(`install exited ${code}`)); resolve(); }); |
| 65 | + }); |
| 66 | +} |
| 67 | + |
| 68 | +async function runFile(file) { |
| 69 | + let src; |
| 70 | + try { src = fs.readFileSync(file, "utf8"); } |
| 71 | + catch (e) { console.log(err(`can't read ${file}: ${e.message}`)); return; } |
| 72 | + let ast; |
| 73 | + try { ast = compile(src); } catch (e) { console.log(err(String(e.message || e))); return; } |
| 74 | + console.log(hr()); |
| 75 | + const ctx = { vars: { alive: true }, iter: 0, maxIterations: 100000, out: (s) => console.log(s), commands: defaultCommands() }; |
| 76 | + try { await run(ast, ctx); } catch (e) { console.log(err(String(e.message || e))); } |
| 77 | + console.log(hr()); |
| 78 | + console.log(info(`moshscript done — ${ctx.iter} loop(s).`)); |
| 79 | +} |
| 80 | + |
| 81 | +export async function tui() { |
| 82 | + console.log(banner()); |
| 83 | + console.log(); |
| 84 | + printEngines(); |
| 85 | + console.log("\n" + ash(" /help for commands · /quit to leave") + "\n"); |
| 86 | + |
| 87 | + let rl = mkrl(); |
| 88 | + for (;;) { |
| 89 | + let line; |
| 90 | + try { line = await ask(rl); } catch { break; } |
| 91 | + if (line == null) break; // Ctrl-D |
| 92 | + line = line.trim(); |
| 93 | + if (!line) continue; |
| 94 | + |
| 95 | + const [raw, ...rest] = line.split(/\s+/); |
| 96 | + const cmd = raw.toLowerCase().replace(/^\//, ""); |
| 97 | + |
| 98 | + if (cmd === "quit" || cmd === "exit" || cmd === "q") break; |
| 99 | + if (cmd === "help" || cmd === "?" || cmd === "h") { printHelp(); continue; } |
| 100 | + if (cmd === "run") { |
| 101 | + if (!rest[0]) { console.log(err("usage: /run <file.mosh>")); continue; } |
| 102 | + await runFile(rest[0]); |
| 103 | + continue; |
| 104 | + } |
| 105 | + if (cmd === "install") { |
| 106 | + if (!rest[0]) { console.log(err("usage: /install <engine>")); continue; } |
| 107 | + rl.close(); |
| 108 | + await installEngine(rest[0].toLowerCase()); |
| 109 | + rl = mkrl(); |
| 110 | + continue; |
| 111 | + } |
| 112 | + if (cmd === "agents" || cmd === "agent" || cmd === "engines") { |
| 113 | + if (!rest[0]) { printEngines(); continue; } |
| 114 | + const resolved = resolveEngine(rest[0]); |
| 115 | + if (!resolved) { console.log(err(`unknown engine "${rest[0]}". try: ${Object.keys(ENGINES).join(", ")}`)); continue; } |
| 116 | + const [key, engine] = resolved; |
| 117 | + rl.close(); |
| 118 | + await openEngine(key, { ...engine, installed: engineStatus().find((e) => e.key === key)?.installed }, rest.slice(1)); |
| 119 | + rl = mkrl(); |
| 120 | + continue; |
| 121 | + } |
| 122 | + // Bare engine name → open it. |
| 123 | + const resolved = resolveEngine(cmd); |
| 124 | + if (resolved) { |
| 125 | + const [key, engine] = resolved; |
| 126 | + rl.close(); |
| 127 | + await openEngine(key, { ...engine, installed: engineStatus().find((e) => e.key === key)?.installed }, rest); |
| 128 | + rl = mkrl(); |
| 129 | + continue; |
| 130 | + } |
| 131 | + console.log(err(`unknown command "${line}". /help for the list.`)); |
| 132 | + } |
| 133 | + |
| 134 | + try { rl.close(); } catch { /* noop */ } |
| 135 | + console.log("\n" + ash("code hard, mosh harder. 🤘")); |
| 136 | +} |
0 commit comments