From 8446bfcae400d5433c096366f36c59ef44c49d7f Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 14 Jul 2026 10:55:58 -0600 Subject: [PATCH 1/3] fix(cli): restore tool registry exports --- bin/moshcode.mjs | 17 ++++++------ src/commands.mjs | 8 ++++-- src/engines.mjs | 59 +++++++++++++++++++++++++++++++++------ src/tools.mjs | 70 ++++++++++++++++++++++++++++++++++++++++++++++- test/cli.test.mjs | 8 +++--- 5 files changed, 137 insertions(+), 25 deletions(-) diff --git a/bin/moshcode.mjs b/bin/moshcode.mjs index 383f6c9..9c882b6 100755 --- a/bin/moshcode.mjs +++ b/bin/moshcode.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node import fs from "node:fs"; -import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { runScript } from "../src/runtime.mjs"; @@ -12,6 +11,7 @@ import { engineStatus, resolveEngine, openSession, + runCmd, } from "../src/engines.mjs"; import { TOOLS, toolList, toolStatus, resolveTool, openTool } from "../src/tools.mjs"; import { runUpgrade } from "../src/upgrade.mjs"; @@ -222,13 +222,14 @@ async function main() { } const { install, desc, bin } = entry; console.log(`šŸŽø installing ${target} — ${desc}\n$ ${install.cmd} ${install.args.join(" ")}\n`); - const child = spawn(install.cmd, install.args, { stdio: "inherit" }); - child.on("error", (e) => { console.error(`install failed: ${e.message}`); process.exit(1); }); - child.on("exit", (code) => { - if (code === 0) console.log(`\nāœ“ ${target} installed. run it with \`${bin}\`. 🤘`); - backToPit(`install ${target}`, code); - }); - return; + const result = await runCmd(install.cmd, install.args); + if (!result.ok) { + console.error(`install failed: ${result.error?.message || result.error || "unknown error"}`); + process.exitCode = 1; + return; + } + if (result.code === 0) console.log(`\nāœ“ ${target} installed. run it with \`${bin}\`. 🤘`); + return backToPit(`install ${target}`, result.code); } if (cmd === "upgrade" || cmd === "update") { console.log("šŸŽø moshcode upgrade — updating moshcode + installed engines/tools 🤘"); diff --git a/src/commands.mjs b/src/commands.mjs index 411d3d4..b291f19 100644 --- a/src/commands.mjs +++ b/src/commands.mjs @@ -171,10 +171,12 @@ const COMMANDS = [ ctx.out(` ā–¶ shell(${JSON.stringify(cmd)}) → would run: $SHELL -c ${JSON.stringify(cmd)}`); return { ok: true, dryRun: true }; } - const sh = process.env.SHELL - || (process.platform === "win32" ? (process.env.COMSPEC || "cmd.exe") : "/bin/sh"); + const sh = process.platform === "win32" + ? (process.env.COMSPEC || "cmd.exe") + : (process.env.SHELL || "/bin/sh"); + const shArgs = process.platform === "win32" ? ["/d", "/s", "/c", cmd] : ["-c", cmd]; ctx.out(` ā–¶ shell: ${cmd}`); - const res = spawnSync(sh, ["-c", cmd], { stdio: "inherit" }); + const res = spawnSync(sh, shArgs, { stdio: "inherit" }); if (res.error) throw res.error; const code = res.status ?? 1; if (code !== 0) { diff --git a/src/engines.mjs b/src/engines.mjs index 536bd78..08cddf7 100644 --- a/src/engines.mjs +++ b/src/engines.mjs @@ -11,7 +11,7 @@ // `agentsView` fall back to `agentArgs` — an autonomous session with native // approvals bypassed/auto-approved. import { spawn } from "node:child_process"; -import { existsSync, statSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; export const ENGINES = { @@ -82,15 +82,54 @@ export function resolveEngine(token) { return key ? [key, ENGINES[key]] : null; } -/** Is `bin` an executable on PATH? (cross-platform-ish) */ -export function isInstalled(bin) { - const exts = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""]; - for (const dir of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) { +function executableCandidates(bin) { + const exts = process.platform === "win32" ? ["", ...(process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";")] : [""]; + const dirs = path.isAbsolute(bin) || bin.includes(path.sep) ? [""] : (process.env.PATH || "").split(path.delimiter).filter(Boolean); + const seen = new Set(); + const candidates = []; + for (const dir of dirs) { for (const ext of exts) { - try { if (existsSync(path.join(dir, bin + ext)) && statSync(path.join(dir, bin + ext)).isFile()) return true; } catch { /* keep looking */ } + const candidate = dir ? path.join(dir, bin + ext) : bin + ext; + const key = candidate.toLowerCase(); + if (!seen.has(key)) { + seen.add(key); + candidates.push(candidate); + } } } - return false; + return candidates; +} + +function resolveExecutable(bin) { + for (const candidate of executableCandidates(bin)) { + try { + if (existsSync(candidate) && statSync(candidate).isFile()) return candidate; + } catch { /* keep looking */ } + } + return null; +} + +function nodeShebang(file) { + try { + const head = readFileSync(file, "utf8").slice(0, 80); + return /^#!.*\bnode(?:\.exe)?\b/.test(head); + } catch { + return false; + } +} + +function spawnSpec(bin, args = []) { + const resolved = resolveExecutable(bin); + if (!resolved) return { cmd: bin, args }; + if (process.platform === "win32" && path.extname(resolved) === "" && nodeShebang(resolved)) { + return { cmd: process.execPath, args: [resolved, ...args] }; + } + return { cmd: resolved, args }; +} + +/** Is `bin` an executable on PATH? (cross-platform-ish) */ +export function isInstalled(bin) { + return Boolean(resolveExecutable(bin)); } // Headless "run one prompt, print the answer, exit" invocation per engine — the @@ -147,7 +186,8 @@ export function agentLaunchArgs(engine, args = []) { export function runCmd(cmd, args = []) { return new Promise((resolve) => { let child; - try { child = spawn(cmd, args, { stdio: "inherit" }); } + const spec = spawnSpec(cmd, args); + try { child = spawn(spec.cmd, spec.args, { stdio: "inherit" }); } catch (e) { resolve({ ok: false, error: e }); return; } child.on("error", (e) => resolve({ ok: false, error: e })); child.on("exit", (code, signal) => resolve({ ok: true, code, signal })); @@ -168,7 +208,8 @@ export function openPassthrough(target, args = []) { for (const k of target.stripEnv) delete env[k]; } let child; - try { child = spawn(target.bin, args, { stdio: "inherit", env }); } + const spec = spawnSpec(target.bin, args); + try { child = spawn(spec.cmd, spec.args, { stdio: "inherit", env }); } catch (e) { resolve({ ok: false, error: e }); return; } child.on("error", (e) => resolve({ ok: false, error: e })); child.on("exit", (code, signal) => resolve({ ok: true, code, signal })); diff --git a/src/tools.mjs b/src/tools.mjs index 1383d6b..f52c308 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -1,4 +1,72 @@ -// tools.mjs - Moshcode utility functions +// Adjacent workflow CLIs moshcode can install and transparently invoke. +// These are deliberately separate from coding engines: UGig owns marketplace +// workflows, CoinPay owns payment workflows, c0mpute owns the compute network, +// and moshcode only conducts their native command lines. +import { isInstalled, openPassthrough } from "./engines.mjs"; + +export const TOOLS = { + ugig: { + desc: "UGig — freelance marketplace CLI for humans and agents", + bin: "ugig", + // UGig isn't published to npm — it ships via its own install script. + install: { cmd: "bash", args: ["-c", "curl -fsSL https://ugig.net/install.sh | bash"] }, + }, + coinpay: { + desc: "CoinPay — wallets, payments, swaps, escrow, and settlement", + bin: "coinpay", + // CoinPay ships via its own install script (fetched from GitHub), not npm. + install: { cmd: "sh", args: ["-c", "curl -fsSL https://coinpayportal.com/install.sh | sh"] }, + }, + c0mpute: { + desc: "c0mpute — decentralized compute network CLI", + bin: "c0mpute", + // c0mpute ships via its own install script (the v1 stack installer). + install: { cmd: "sh", args: ["-c", "curl -fsSL https://c0mpute.com/install.sh | sh"] }, + }, + secrets: { + desc: "LogicSRC — end-to-end-encrypted team credential sharing (login, teams, credentials)", + // The passthrough target is the `logicsrc` binary; the moshcode command is + // `/secrets` so it reads as "manage secrets". LOGICSRC_BIN points at a local + // build before logicsrc ships a global install. + bin: process.env.LOGICSRC_BIN || "logicsrc", + // LogicSRC ships via its own install script (same pattern as the others). + install: { cmd: "sh", args: ["-c", "curl -fsSL https://logicsrc.com/install.sh | sh"] }, + }, +}; + +/** Resolve a name to `[key, tool]`, or null. */ +export function resolveTool(token) { + if (!token) return null; + const key = String(token).trim().toLowerCase(); + return TOOLS[key] ? [key, TOOLS[key]] : null; +} + +/** Tool entries annotated with native executable install status. */ +export function toolStatus() { + return Object.entries(TOOLS).map(([key, tool]) => ({ + key, + ...tool, + installed: isInstalled(tool.bin), + })); +} + +export function toolList() { + return Object.entries(TOOLS) + .map(([key, tool]) => ` ${key.padEnd(10)} ${tool.desc}`) + .join("\n"); +} + +/** Prefer a native updater when one is added; npm installs are idempotent. */ +export function toolUpgradeSpec(tool) { + return tool.upgrade || tool.install; +} + +/** Invoke a tool without parsing or modifying its arguments or streams. */ +export function openTool(tool, args = []) { + return openPassthrough(tool, args); +} + +// Generic utilities used by the app/package surface. /** * Format a number as currency diff --git a/test/cli.test.mjs b/test/cli.test.mjs index dfc3ca0..99faa0e 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -164,7 +164,7 @@ test("shell() runs a real command and returns { ok, code }", () => { const lines = []; const ctx = { dryRun: false, out: (l) => lines.push(l) }; const cmd = moshVocabulary().get("shell"); - const result = cmd.run(ctx, "true"); + const result = cmd.run(ctx, "node -e process.exitCode=0"); assert.equal(result.ok, true); assert.equal(result.code, 0); }); @@ -173,15 +173,15 @@ test("shell() returns { ok: false } on non-zero exit without throwing", () => { const lines = []; const ctx = { dryRun: false, out: (l) => lines.push(l) }; const cmd = moshVocabulary().get("shell"); - const result = cmd.run(ctx, "false"); + const result = cmd.run(ctx, "node -e process.exitCode=7"); assert.equal(result.ok, false); - assert.ok(result.code !== 0); + assert.equal(result.code, 7); }); test("shell() is callable from moshscript and the script continues on failure", async () => { const lines = []; await runScript( - `const r = shell("false"); say("continued, ok=" + r.ok);`, + `const r = shell("node -e process.exitCode=7"); say("continued, ok=" + r.ok);`, { commands: moshVocabulary(), out: (s) => lines.push(s) } ); assert.match(lines.join("\n"), /continued, ok=false/); From de4244bdf1e9fe4dde883bffe76a773dfb2b2a35 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 14 Jul 2026 11:07:11 -0600 Subject: [PATCH 2/3] fix(cli): print package version --- bin/moshcode.mjs | 6 ++++++ test/cli.test.mjs | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/bin/moshcode.mjs b/bin/moshcode.mjs index 9c882b6..80afd6a 100755 --- a/bin/moshcode.mjs +++ b/bin/moshcode.mjs @@ -20,6 +20,7 @@ import { locate, tilde } from "../src/pwd.mjs"; import { createPrd, listPrds, authoringPrompt } from "../src/prd.mjs"; import { login, loginDevice, whoami, logout } from "../src/auth.mjs"; import { tui } from "../src/tui.mjs"; +import { moshcodeVersion } from "../src/ui.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXAMPLE = path.join(HERE, "..", "examples", "alive.mosh"); @@ -171,6 +172,11 @@ async function main() { // No args → open the interactive TUI shell (/agents , etc.). if (cmd === undefined) return tui(); + if (cmd === "--version" || cmd === "-v" || cmd === "version") { + console.log(moshcodeVersion() || "unknown"); + return; + } + if (cmd === "engines") { printEngineStatus(); return; diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 99faa0e..2fe7f2a 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -1,5 +1,8 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { runMoshcode, cliVerb, runAi } from "../src/cli.mjs"; import { aiExecArgs, pickAiEngine } from "../src/engines.mjs"; @@ -7,6 +10,16 @@ import { moshVocabulary } from "../src/commands.mjs"; import { runScript } from "../src/runtime.mjs"; import { createRegistry } from "../src/registry.mjs"; +test("moshcode --version prints the package version", () => { + const expected = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version; + const result = spawnSync(process.execPath, [fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url)), "--version"], { + encoding: "utf8", + }); + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), expected); + assert.equal(result.stderr, ""); +}); + function dryCtx() { return { dryRun: true, lines: [], out(l) { this.lines.push(l); } }; } From c69d859d5e4dbe1614355c9c29ad7e7faac19b2f Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 14 Jul 2026 23:00:45 -0600 Subject: [PATCH 3/3] docs(cli): clarify shell command behavior --- src/commands.mjs | 2 +- test/commands.test.mjs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands.mjs b/src/commands.mjs index b291f19..aa69f35 100644 --- a/src/commands.mjs +++ b/src/commands.mjs @@ -158,7 +158,7 @@ const COMMANDS = [ { name: "shell", - summary: "run a shell command (blocking, spawnSync $SHELL -c)", + summary: "run a shell command (blocking, cmd.exe on Windows or $SHELL -c elsewhere)", // The moshscript system verb for arbitrary shell commands. Blocking // (spawnSync + inherited stdio) so it runs inline in the no-`await` style, // and the child owns the terminal for interactive commands. Returns diff --git a/test/commands.test.mjs b/test/commands.test.mjs index a794b55..2f9c3ef 100644 --- a/test/commands.test.mjs +++ b/test/commands.test.mjs @@ -83,3 +83,9 @@ test("the vocabulary exposes summaries for `moshcode commands`", () => { assert.ok(cmd.summary.length > 0, `${cmd.name}() needs a summary`); } }); + +test("shell() summary describes the portable shell behavior", () => { + const cmd = moshVocabulary().get("shell"); + assert.match(cmd.summary, /cmd\.exe on Windows/); + assert.match(cmd.summary, /\$SHELL -c elsewhere/); +});