From c912386a052597908a3759ddfe8761a080e10489 Mon Sep 17 00:00:00 2001 From: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:33:22 +0000 Subject: [PATCH] fix(engines): stop a nested moshcode child opening a second TUI A CLI verb called from a script (run(), install(), agents(), upgrade()) spawns `moshcode ` with stdio inherited, so the child sees a TTY whenever a human runs the script from a terminal. At the end of the verb the child called backToPit(), printed the banner and blocked on its own `mosh >` prompt, so the parent script never resumed. Mark spawned children with MOSHCODE_NESTED=1 and treat that like a non-TTY in backToPit, so a nested run exits and hands control back. Same fix for the moshscript shim, which made an executable .mosh file land in a TUI instead of returning to the user's shell. --- bin/moshcode.mjs | 5 ++++- bin/moshscript.mjs | 5 ++++- src/cli.mjs | 5 ++++- test/run-options.test.mjs | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/bin/moshcode.mjs b/bin/moshcode.mjs index 80afd6a..a000679 100755 --- a/bin/moshcode.mjs +++ b/bin/moshcode.mjs @@ -56,7 +56,10 @@ function parseMax(value) { // interactive. Piped / non-TTY invocations (scripts, CI, `… | moshcode run -`) // keep the old behaviour: exit with the child's code. function backToPit(label, code, signal) { - if (!process.stdin.isTTY) process.exit(code ?? 0); + // A nested invocation (moshscript shim, or a CLI verb called from a script) + // must hand control back to its parent, not open a second mosh pit on the + // shared TTY. + if (!process.stdin.isTTY || process.env.MOSHCODE_NESTED === "1") process.exit(code ?? 0); const how = signal ? ` (${signal})` : code != null ? ` (code ${code})` : ""; console.log(`\n↩ ${label} exited${how} — back in the mosh pit. /quit to leave.\n`); return tui(); diff --git a/bin/moshscript.mjs b/bin/moshscript.mjs index 7db74c2..3629b97 100755 --- a/bin/moshscript.mjs +++ b/bin/moshscript.mjs @@ -14,7 +14,10 @@ import { fileURLToPath } from "node:url"; const BIN = fileURLToPath(new URL("moshcode.mjs", import.meta.url)); const args = process.argv.slice(2); // everything after `moshscript` -const child = spawn(process.execPath, [BIN, "run", ...args], { stdio: "inherit" }); +const child = spawn(process.execPath, [BIN, "run", ...args], { + stdio: "inherit", + env: { ...process.env, MOSHCODE_NESTED: "1" }, +}); child.on("error", (e) => { console.error(`moshscript: ${e.message}`); process.exit(1); }); child.on("exit", (code, signal) => { if (signal) { diff --git a/src/cli.mjs b/src/cli.mjs index 3605ac8..f875a1f 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -41,7 +41,10 @@ export function runMoshcode(cmd, args, ctx) { } ctx.out(` ▶ ${printable}`); - const res = spawnSync(process.execPath, [MOSHCODE_BIN, ...argv], { stdio: "inherit" }); + const res = spawnSync(process.execPath, [MOSHCODE_BIN, ...argv], { + stdio: "inherit", + env: { ...process.env, MOSHCODE_NESTED: "1" }, + }); if (res.error) throw res.error; // truly fatal: spawn itself failed (ENOENT etc.) const code = res.status ?? 1; diff --git a/test/run-options.test.mjs b/test/run-options.test.mjs index 7d1b7ee..c6171ad 100644 --- a/test/run-options.test.mjs +++ b/test/run-options.test.mjs @@ -49,6 +49,21 @@ test("run accepts equals-form max option", async () => { assert.match(result.stdout, /1 loop\(s\)/); }); +test("run() marks the nested moshcode child as nested", async () => { + const dir = mkdtempSync(join(tmpdir(), "moshcode-nested-")); + const child = join(dir, "child.mosh"); + const parent = join(dir, "parent.mosh"); + writeFileSync(child, 'shell("echo NESTEDVAL=$MOSHCODE_NESTED");\n'); + writeFileSync(parent, `run(${JSON.stringify(child)});\n`); + + const result = await run([parent]); + + assert.equal(result.status, 0); + // Without MOSHCODE_NESTED the child drops into its own TUI on a shared TTY + // instead of returning control to the parent script. + assert.match(result.stdout, /NESTEDVAL=1/); +}); + test("run() includes another .mosh file, in order", async () => { const dir = mkdtempSync(join(tmpdir(), "moshcode-include-")); const child = join(dir, "child.mosh");