From 92af32eaa11bb7da34adf8c758d380670d56f5f8 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Mon, 27 Jul 2026 10:37:56 +0700 Subject: [PATCH] feat(cli): add JSON install status output --- README.md | 2 ++ bin/moshcode.mjs | 29 ++++++++++++++++++++--------- test/cli.test.mjs | 24 +++++++++++++++++++++++- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a5ce554..279c1a6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ to upgrade, `… | sh -s -- remove` to uninstall. ```sh moshcode engines # list installable engines +moshcode engines --json # machine-readable install status for automation moshcode install opencode # install opencode (curl … | bash) moshcode install claude # npm i -g @anthropic-ai/claude-code moshcode install codex # npm i -g @openai/codex @@ -62,6 +63,7 @@ installs them and passes control through without reimplementing their APIs. ```sh moshcode tools # list tools and native install status +moshcode tools --json # machine-readable install status for automation moshcode install ugig # npm install -g ugig moshcode install coinpay # npm install -g @profullstack/coinpay diff --git a/bin/moshcode.mjs b/bin/moshcode.mjs index c28a940..f095889 100755 --- a/bin/moshcode.mjs +++ b/bin/moshcode.mjs @@ -76,12 +76,25 @@ function propagateExit(code, signal) { process.exitCode = code ?? 0; } -function printEngineStatus() { - for (const engine of engineStatus()) { - console.log(`${engine.installed ? "●" : "○"} ${engine.key.padEnd(10)} ${engine.desc}`); +function printStatus(entries, json = false) { + if (json) { + console.log(JSON.stringify(entries.map(({ key, desc, bin, installed }) => ({ + name: key, + description: desc, + binary: bin, + installed, + })), null, 2)); + return; + } + for (const entry of entries) { + console.log(`${entry.installed ? "●" : "○"} ${entry.key.padEnd(10)} ${entry.desc}`); } } +function printEngineStatus(json = false) { + printStatus(engineStatus(), json); +} + async function launchEngine(key, engine, args, { agentMode = false } = {}) { if (agentMode) { console.error(`⚠ agent mode: ${key} ${agentLaunchArgs(engine).join(" ")}${engine.agentsView ? " — opening its agent view" : " — native approvals/permissions are bypassed or auto-approved"}.`); @@ -133,8 +146,8 @@ usage: code flow) so notify()/ask() reach you moshcode whoami | logout show / clear the logged-in account moshcode pwd show the current dir + git repo/branch/origin - moshcode engines list engines + install status - moshcode tools list workflow tools + install status + moshcode engines [--json] list engines + install status + moshcode tools [--json] list workflow tools + install status moshcode commands list built-in moshscript commands moshcode help this @@ -181,7 +194,7 @@ async function main() { } if (cmd === "engines") { - printEngineStatus(); + printEngineStatus(rest.includes("--json")); return; } if (cmd === "agents") { @@ -211,9 +224,7 @@ async function main() { return launchEngine(key, engine, rest.slice(1)); } if (cmd === "tools") { - for (const tool of toolStatus()) { - console.log(`${tool.installed ? "●" : "○"} ${tool.key.padEnd(10)} ${tool.desc}`); - } + printStatus(toolStatus(), rest.includes("--json")); return; } if (cmd === "mcp") { diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 2fe7f2a..36a9b6a 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -10,9 +10,11 @@ import { moshVocabulary } from "../src/commands.mjs"; import { runScript } from "../src/runtime.mjs"; import { createRegistry } from "../src/registry.mjs"; +const BIN = fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url)); + 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"], { + const result = spawnSync(process.execPath, [BIN, "--version"], { encoding: "utf8", }); assert.equal(result.status, 0); @@ -20,6 +22,26 @@ test("moshcode --version prints the package version", () => { assert.equal(result.stderr, ""); }); +for (const command of ["engines", "tools"]) { + test(`moshcode ${command} --json prints machine-readable install status`, () => { + const result = spawnSync(process.execPath, [BIN, command, "--json"], { + encoding: "utf8", + }); + + assert.equal(result.status, 0); + assert.equal(result.stderr, ""); + const entries = JSON.parse(result.stdout); + assert.ok(entries.length > 0); + for (const entry of entries) { + assert.deepEqual(Object.keys(entry), ["name", "description", "binary", "installed"]); + assert.equal(typeof entry.name, "string"); + assert.equal(typeof entry.description, "string"); + assert.equal(typeof entry.binary, "string"); + assert.equal(typeof entry.installed, "boolean"); + } + }); +} + function dryCtx() { return { dryRun: true, lines: [], out(l) { this.lines.push(l); } }; }