Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions bin/moshcode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 <engine|tool>\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`);
Expand Down Expand Up @@ -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 <engine|tool>\nengines:\n${engineList()}\ntools:\n${toolList()}`);
process.exit(target ? 1 : 0);
process.exit(token ? 1 : 0);
}

const binPath = resolveExecutable(entry.bin, entry.binDirs);
Expand Down
16 changes: 15 additions & 1 deletion src/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]) => ({
Expand Down
14 changes: 8 additions & 6 deletions src/tui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 15 additions & 2 deletions test/tools.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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);
});
Loading