Skip to content

Commit de8665f

Browse files
ralyodioclaude
andcommitted
feat(tui): /shell — drop to $SHELL and return to the pit (v0.10.0)
vim `:sh`-style shell escape for the mosh TUI. `/shell` (alias `/sh`) hands the terminal to your $SHELL; exiting the shell (`exit`/Ctrl-D) lands you right back at the `mosh ▸` prompt with the whole TUI session — history, running context — intact, so you don't have to quit moshcode just to run a shell command. Also: `/shell <cmd>` runs a one-off via `$SHELL -c`, and a vim-style `!cmd` shortcut does the same (bare `!` drops into an interactive shell). The `!` path uses the raw line so quoting is preserved. Same close/reopen-readline hand-off pattern as engine sessions; cwd + env inherited. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 74bcfe2 commit de8665f

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "moshcode",
3-
"version": "0.9.3",
3+
"version": "0.10.0",
44
"type": "module",
55
"description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
66
"bin": {

src/tui.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ function printHelp() {
127127
` ${acid("/install <name>")} install an engine or workflow tool`,
128128
` ${acid("/upgrade [name…]")} update moshcode + installed engines/tools (or named targets)`,
129129
` ${acid("/pwd")} show the current dir + git repo/branch/origin`,
130+
` ${acid("/shell [cmd]")} drop into $SHELL (exit → back to the pit); also ${acid("!cmd")}`,
130131
` ${acid("/prd [idea]")} publish a numbered PRD (OpenPRD), or list them with no arg`,
131132
` ${acid("/run <file.mosh>")} run a moshscript program`,
132133
` ${acid("/help")} this`,
@@ -190,6 +191,39 @@ async function openWorkflowTool(key, tool, args) {
190191
}
191192
}
192193

194+
// Spawn the user's shell with the terminal fully handed over (stdio inherit),
195+
// inheriting the current cwd + env. No args → an interactive shell; a raw
196+
// command string → `$SHELL -c "<cmd>"` (one-off). Resolves { ok, code, signal }.
197+
function runShell(rawCmd) {
198+
return new Promise((resolve) => {
199+
const shell = process.env.SHELL
200+
|| (process.platform === "win32" ? (process.env.COMSPEC || "cmd.exe") : "/bin/sh");
201+
const args = rawCmd ? ["-c", rawCmd] : [];
202+
let child;
203+
try { child = spawn(shell, args, { stdio: "inherit" }); }
204+
catch (e) { resolve({ ok: false, error: e }); return; }
205+
child.on("error", (e) => resolve({ ok: false, error: e }));
206+
child.on("exit", (code, signal) => resolve({ ok: true, code, signal }));
207+
});
208+
}
209+
210+
// vim `:sh` — drop into a shell and land back at the mosh prompt on exit, with
211+
// the whole TUI session (history, cwd) intact. `rawCmd` runs a one-off instead.
212+
async function openShell(rawCmd) {
213+
const shellName = path.basename(process.env.SHELL || "sh");
214+
console.log(info(rawCmd
215+
? `${bone(shellName)} ${ash("-c")} ${ash(rawCmd)}`
216+
: `dropping to ${bone(shellName)}${ash("`exit` or Ctrl-D brings you back to the pit")}`));
217+
console.log(hr());
218+
const r = await runShell(rawCmd);
219+
console.log(hr());
220+
if (!r.ok) {
221+
console.log(err(`couldn't start shell: ${r.error?.message || r.error}`));
222+
} else {
223+
console.log(info(`shell exited${r.code != null ? ` (code ${r.code})` : r.signal ? ` (${r.signal})` : ""}. back in the pit.`));
224+
}
225+
}
226+
193227
function installTarget(key) {
194228
return new Promise((resolve) => {
195229
const target = ENGINES[key] || TOOLS[key];
@@ -249,6 +283,16 @@ export async function tui() {
249283
if (!line) continue;
250284
saveHistory(); // readline just recorded this line into the shared history
251285

286+
// vim-style shell escape: `!` drops into $SHELL, `!<cmd>` runs one-off. We
287+
// take the raw remainder (not the tokenized parts) so quoting is preserved.
288+
if (line.startsWith("!")) {
289+
const rawCmd = line.slice(1).trim();
290+
rl.close();
291+
await openShell(rawCmd || null);
292+
rl = mkrl();
293+
continue;
294+
}
295+
252296
let parts;
253297
try { parts = splitCommandLine(line); }
254298
catch (error) { console.log(err(`can't parse command: ${error.message}`)); continue; }
@@ -263,6 +307,12 @@ export async function tui() {
263307
await runFile(rest[0]);
264308
continue;
265309
}
310+
if (cmd === "shell" || cmd === "sh") {
311+
rl.close();
312+
await openShell(rest.length ? rest.join(" ") : null);
313+
rl = mkrl();
314+
continue;
315+
}
266316
if (cmd === "install") {
267317
if (!rest[0]) { console.log(err("usage: /install <engine|tool>")); continue; }
268318
rl.close();

0 commit comments

Comments
 (0)