From 7745bdbb7c2a0df102abe9d0a227ee54bbd4d87c Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 22 Sep 2026 10:00:43 +0000 Subject: [PATCH] fix(install): resolve engine aliases in /install and moshcode install `/install mimo` and `moshcode install cc` looked the token up as a raw ENGINES key and printed "unknown engine", while `/agents mimo`, `/start cc` and `moshcode upgrade cc` all took the alias. One resolver (resolveInstallable: engine name or alias, else tool key) now backs the pit's /install and the CLI's install and uninstall, so the name that starts an engine is also the name that installs it. Prototype names still print the unknown-target line. Co-Authored-By: Claude Fable 5.1 --- bin/moshcode.mjs | 29 ++++++++++++++++------------- src/tools.mjs | 16 +++++++++++++++- src/tui.mjs | 14 ++++++++------ test/tools.test.mjs | 17 +++++++++++++++-- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/bin/moshcode.mjs b/bin/moshcode.mjs index d9f5e7e4..61f04270 100755 --- a/bin/moshcode.mjs +++ b/bin/moshcode.mjs @@ -15,7 +15,7 @@ import { resolveExecutable, runCmd, } from "../src/engines.mjs"; -import { TOOLS, toolList, toolStatus, resolveTool, openTool, adoptAliasLines } from "../src/tools.mjs"; +import { TOOLS, toolList, toolStatus, resolveTool, resolveInstallable, openTool, adoptAliasLines } from "../src/tools.mjs"; import { tradeArgs, tradeUsage } from "../src/trade.mjs"; import { runUpgrade } from "../src/upgrade.mjs"; import { selfUpdateCommand } from "../src/selfupdate.mjs"; @@ -514,14 +514,16 @@ async function main() { return; } if (cmd === "install") { - const target = rest.find((a) => !a.startsWith("-"))?.toLowerCase(); - // Own properties only — `install constructor` must print usage, not resolve - // to something off Object.prototype and crash on its missing install spec. - const entry = target - && ((Object.hasOwn(ENGINES, target) && ENGINES[target]) || (Object.hasOwn(TOOLS, target) && TOOLS[target])); - if (!target || !entry) { + const token = rest.find((a) => !a.startsWith("-"))?.toLowerCase(); + // Aliases resolve here as on every other engine surface (`install mimo`, + // `install cc`), and the resolvers check own properties only — `install + // constructor` prints usage rather than crashing on a missing install spec. + const resolved = token ? resolveInstallable(token) : null; + const target = resolved?.[0]; + const entry = resolved?.[1]; + if (!token || !entry) { console.error(`usage: moshcode install \nengines:\n${engineList()}\ntools:\n${toolList()}`); - process.exit(target ? 1 : 0); + process.exit(token ? 1 : 0); } const { install, desc, bin } = entry; console.log(`🎸 installing ${target} — ${desc}\n$ ${install.cmd} ${install.args.join(" ")}\n`); @@ -552,12 +554,13 @@ async function main() { return backToPit(`install ${target}`, result.code); } if (cmd === "uninstall" || cmd === "remove") { - const target = rest.find((a) => !a.startsWith("-"))?.toLowerCase(); - const entry = target - && ((Object.hasOwn(ENGINES, target) && ENGINES[target]) || (Object.hasOwn(TOOLS, target) && TOOLS[target])); - if (!target || !entry) { + const token = rest.find((a) => !a.startsWith("-"))?.toLowerCase(); + const resolved = token ? resolveInstallable(token) : null; + const target = resolved?.[0]; + const entry = resolved?.[1]; + if (!token || !entry) { console.error(`usage: moshcode uninstall \nengines:\n${engineList()}\ntools:\n${toolList()}`); - process.exit(target ? 1 : 0); + process.exit(token ? 1 : 0); } const binPath = resolveExecutable(entry.bin, entry.binDirs); diff --git a/src/tools.mjs b/src/tools.mjs index 8fd6edf7..fcaf4abe 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -10,7 +10,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { mergeAliases } from "./aliases.mjs"; -import { isInstalled, openPassthrough } from "./engines.mjs"; +import { isInstalled, openPassthrough, resolveEngine } from "./engines.mjs"; // gh, supabase, and doctl publish only GitHub release binaries — no official // cross-platform install script between them — so their install spec runs our @@ -464,6 +464,20 @@ export function resolveTool(token) { return Object.hasOwn(TOOLS, key) ? [key, TOOLS[key]] : null; } +/** + * Resolve an install/uninstall target — an engine (by name or alias) or a + * tool — to `[key, entry]`, or null. + * + * `/install mimo` and `moshcode install cc` used to look the token up as a + * raw ENGINES key and print "unknown engine", while `/agents mimo`, + * `moshcode start cc` and `moshcode upgrade cc` all took the alias. One + * resolver for both install paths, so the name that starts an engine is also + * the name that installs it. + */ +export function resolveInstallable(token) { + return resolveEngine(token) || resolveTool(token); +} + /** Tool entries annotated with native executable install status. */ export function toolStatus() { return Object.entries(TOOLS).map(([key, tool]) => ({ diff --git a/src/tui.mjs b/src/tui.mjs index e6d09553..eee34208 100644 --- a/src/tui.mjs +++ b/src/tui.mjs @@ -8,7 +8,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { ENGINES, agentLaunchArgs, resolveEngine, engineStatus, openSession } from "./engines.mjs"; -import { TOOLS, resolveTool, toolStatus, openTool, readToolAliases, toolsWithAliases } from "./tools.mjs"; +import { TOOLS, resolveTool, resolveInstallable, toolStatus, openTool, readToolAliases, toolsWithAliases } from "./tools.mjs"; import { tradeArgs, tradeUsage } from "./trade.mjs"; import { postSocial, socialRoster } from "./socials.mjs"; import { shortenCommand } from "./shorten.mjs"; @@ -790,12 +790,14 @@ async function openShell(rawCmd) { } } -function installTarget(key) { +function installTarget(token) { return new Promise((resolve) => { - // Own properties only — `/install constructor` must print the unknown-target - // line, not resolve to something off Object.prototype and crash the pit. - const target = (Object.hasOwn(ENGINES, key) && ENGINES[key]) || (Object.hasOwn(TOOLS, key) && TOOLS[key]); - if (!target) { console.log(err(`unknown engine or tool "${key}"`)); return resolve(); } + // Aliases resolve here as they do for `/agents mimo` and `/start cc`, and + // the resolvers check own properties only — `/install constructor` prints + // the unknown-target line, not a TypeError that kills the pit. + const resolved = resolveInstallable(token); + if (!resolved) { console.log(err(`unknown engine or tool "${token}"`)); return resolve(); } + const [key, target] = resolved; console.log(info(`installing ${key}: ${target.install.cmd} ${target.install.args.join(" ")}`)); // Before the rule, so the prompt reads as the pit asking rather than as // something the installer's output scrolled into view. diff --git a/test/tools.test.mjs b/test/tools.test.mjs index 2332d4ae..b8356ba7 100644 --- a/test/tools.test.mjs +++ b/test/tools.test.mjs @@ -16,9 +16,9 @@ import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import test from "node:test"; -import { isInstalled, primaryBin, resolveEngine } from "../src/engines.mjs"; +import { isInstalled, primaryBin, resolveEngine, ENGINES } from "../src/engines.mjs"; import { needsRootHere } from "../src/escalate.mjs"; -import { TOOLS, resolveTool, retry, toolList, toolUpgradeSpec } from "../src/tools.mjs"; +import { TOOLS, resolveInstallable, resolveTool, retry, toolList, toolUpgradeSpec } from "../src/tools.mjs"; const BIN = fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url)); @@ -671,3 +671,16 @@ test("primaryBin names one command, so a list never reaches a message or a spawn assert.equal(primaryBin(["magick", "convert"]), "magick"); assert.equal(primaryBin("ffmpeg"), "ffmpeg"); }); + +test("install targets resolve engine aliases the way every other engine surface does", () => { + // `/install mimo` and `moshcode install cc` looked the token up as a raw + // ENGINES key and printed "unknown engine" while `/agents mimo` worked. + assert.deepEqual(resolveInstallable("mimo"), ["mimocode", ENGINES.mimocode]); + assert.deepEqual(resolveInstallable("cc"), ["claude", ENGINES.claude]); + assert.deepEqual(resolveInstallable("MIMOCODE"), ["mimocode", ENGINES.mimocode]); + // tools still resolve by their own key + assert.deepEqual(resolveInstallable("ugig"), ["ugig", TOOLS.ugig]); + // and an Object.prototype name is still nothing + assert.equal(resolveInstallable("constructor"), null); + assert.equal(resolveInstallable(""), null); +});