Skip to content

Commit 8446bfc

Browse files
committed
fix(cli): restore tool registry exports
1 parent c0840ea commit 8446bfc

5 files changed

Lines changed: 137 additions & 25 deletions

File tree

bin/moshcode.mjs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env node
22
import fs from "node:fs";
3-
import { spawn } from "node:child_process";
43
import { fileURLToPath } from "node:url";
54
import path from "node:path";
65
import { runScript } from "../src/runtime.mjs";
@@ -12,6 +11,7 @@ import {
1211
engineStatus,
1312
resolveEngine,
1413
openSession,
14+
runCmd,
1515
} from "../src/engines.mjs";
1616
import { TOOLS, toolList, toolStatus, resolveTool, openTool } from "../src/tools.mjs";
1717
import { runUpgrade } from "../src/upgrade.mjs";
@@ -222,13 +222,14 @@ async function main() {
222222
}
223223
const { install, desc, bin } = entry;
224224
console.log(`🎸 installing ${target}${desc}\n$ ${install.cmd} ${install.args.join(" ")}\n`);
225-
const child = spawn(install.cmd, install.args, { stdio: "inherit" });
226-
child.on("error", (e) => { console.error(`install failed: ${e.message}`); process.exit(1); });
227-
child.on("exit", (code) => {
228-
if (code === 0) console.log(`\n✓ ${target} installed. run it with \`${bin}\`. 🤘`);
229-
backToPit(`install ${target}`, code);
230-
});
231-
return;
225+
const result = await runCmd(install.cmd, install.args);
226+
if (!result.ok) {
227+
console.error(`install failed: ${result.error?.message || result.error || "unknown error"}`);
228+
process.exitCode = 1;
229+
return;
230+
}
231+
if (result.code === 0) console.log(`\n✓ ${target} installed. run it with \`${bin}\`. 🤘`);
232+
return backToPit(`install ${target}`, result.code);
232233
}
233234
if (cmd === "upgrade" || cmd === "update") {
234235
console.log("🎸 moshcode upgrade — updating moshcode + installed engines/tools 🤘");

src/commands.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,12 @@ const COMMANDS = [
171171
ctx.out(` ▶ shell(${JSON.stringify(cmd)}) → would run: $SHELL -c ${JSON.stringify(cmd)}`);
172172
return { ok: true, dryRun: true };
173173
}
174-
const sh = process.env.SHELL
175-
|| (process.platform === "win32" ? (process.env.COMSPEC || "cmd.exe") : "/bin/sh");
174+
const sh = process.platform === "win32"
175+
? (process.env.COMSPEC || "cmd.exe")
176+
: (process.env.SHELL || "/bin/sh");
177+
const shArgs = process.platform === "win32" ? ["/d", "/s", "/c", cmd] : ["-c", cmd];
176178
ctx.out(` ▶ shell: ${cmd}`);
177-
const res = spawnSync(sh, ["-c", cmd], { stdio: "inherit" });
179+
const res = spawnSync(sh, shArgs, { stdio: "inherit" });
178180
if (res.error) throw res.error;
179181
const code = res.status ?? 1;
180182
if (code !== 0) {

src/engines.mjs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// `agentsView` fall back to `agentArgs` — an autonomous session with native
1212
// approvals bypassed/auto-approved.
1313
import { spawn } from "node:child_process";
14-
import { existsSync, statSync } from "node:fs";
14+
import { existsSync, readFileSync, statSync } from "node:fs";
1515
import path from "node:path";
1616

1717
export const ENGINES = {
@@ -82,15 +82,54 @@ export function resolveEngine(token) {
8282
return key ? [key, ENGINES[key]] : null;
8383
}
8484

85-
/** Is `bin` an executable on PATH? (cross-platform-ish) */
86-
export function isInstalled(bin) {
87-
const exts = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
88-
for (const dir of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
85+
function executableCandidates(bin) {
86+
const exts = process.platform === "win32" ? ["", ...(process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";")] : [""];
87+
const dirs = path.isAbsolute(bin) || bin.includes(path.sep) ? [""] : (process.env.PATH || "").split(path.delimiter).filter(Boolean);
88+
const seen = new Set();
89+
const candidates = [];
90+
for (const dir of dirs) {
8991
for (const ext of exts) {
90-
try { if (existsSync(path.join(dir, bin + ext)) && statSync(path.join(dir, bin + ext)).isFile()) return true; } catch { /* keep looking */ }
92+
const candidate = dir ? path.join(dir, bin + ext) : bin + ext;
93+
const key = candidate.toLowerCase();
94+
if (!seen.has(key)) {
95+
seen.add(key);
96+
candidates.push(candidate);
97+
}
9198
}
9299
}
93-
return false;
100+
return candidates;
101+
}
102+
103+
function resolveExecutable(bin) {
104+
for (const candidate of executableCandidates(bin)) {
105+
try {
106+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
107+
} catch { /* keep looking */ }
108+
}
109+
return null;
110+
}
111+
112+
function nodeShebang(file) {
113+
try {
114+
const head = readFileSync(file, "utf8").slice(0, 80);
115+
return /^#!.*\bnode(?:\.exe)?\b/.test(head);
116+
} catch {
117+
return false;
118+
}
119+
}
120+
121+
function spawnSpec(bin, args = []) {
122+
const resolved = resolveExecutable(bin);
123+
if (!resolved) return { cmd: bin, args };
124+
if (process.platform === "win32" && path.extname(resolved) === "" && nodeShebang(resolved)) {
125+
return { cmd: process.execPath, args: [resolved, ...args] };
126+
}
127+
return { cmd: resolved, args };
128+
}
129+
130+
/** Is `bin` an executable on PATH? (cross-platform-ish) */
131+
export function isInstalled(bin) {
132+
return Boolean(resolveExecutable(bin));
94133
}
95134

96135
// Headless "run one prompt, print the answer, exit" invocation per engine — the
@@ -147,7 +186,8 @@ export function agentLaunchArgs(engine, args = []) {
147186
export function runCmd(cmd, args = []) {
148187
return new Promise((resolve) => {
149188
let child;
150-
try { child = spawn(cmd, args, { stdio: "inherit" }); }
189+
const spec = spawnSpec(cmd, args);
190+
try { child = spawn(spec.cmd, spec.args, { stdio: "inherit" }); }
151191
catch (e) { resolve({ ok: false, error: e }); return; }
152192
child.on("error", (e) => resolve({ ok: false, error: e }));
153193
child.on("exit", (code, signal) => resolve({ ok: true, code, signal }));
@@ -168,7 +208,8 @@ export function openPassthrough(target, args = []) {
168208
for (const k of target.stripEnv) delete env[k];
169209
}
170210
let child;
171-
try { child = spawn(target.bin, args, { stdio: "inherit", env }); }
211+
const spec = spawnSpec(target.bin, args);
212+
try { child = spawn(spec.cmd, spec.args, { stdio: "inherit", env }); }
172213
catch (e) { resolve({ ok: false, error: e }); return; }
173214
child.on("error", (e) => resolve({ ok: false, error: e }));
174215
child.on("exit", (code, signal) => resolve({ ok: true, code, signal }));

src/tools.mjs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,72 @@
1-
// tools.mjs - Moshcode utility functions
1+
// Adjacent workflow CLIs moshcode can install and transparently invoke.
2+
// These are deliberately separate from coding engines: UGig owns marketplace
3+
// workflows, CoinPay owns payment workflows, c0mpute owns the compute network,
4+
// and moshcode only conducts their native command lines.
5+
import { isInstalled, openPassthrough } from "./engines.mjs";
6+
7+
export const TOOLS = {
8+
ugig: {
9+
desc: "UGig — freelance marketplace CLI for humans and agents",
10+
bin: "ugig",
11+
// UGig isn't published to npm — it ships via its own install script.
12+
install: { cmd: "bash", args: ["-c", "curl -fsSL https://ugig.net/install.sh | bash"] },
13+
},
14+
coinpay: {
15+
desc: "CoinPay — wallets, payments, swaps, escrow, and settlement",
16+
bin: "coinpay",
17+
// CoinPay ships via its own install script (fetched from GitHub), not npm.
18+
install: { cmd: "sh", args: ["-c", "curl -fsSL https://coinpayportal.com/install.sh | sh"] },
19+
},
20+
c0mpute: {
21+
desc: "c0mpute — decentralized compute network CLI",
22+
bin: "c0mpute",
23+
// c0mpute ships via its own install script (the v1 stack installer).
24+
install: { cmd: "sh", args: ["-c", "curl -fsSL https://c0mpute.com/install.sh | sh"] },
25+
},
26+
secrets: {
27+
desc: "LogicSRC — end-to-end-encrypted team credential sharing (login, teams, credentials)",
28+
// The passthrough target is the `logicsrc` binary; the moshcode command is
29+
// `/secrets` so it reads as "manage secrets". LOGICSRC_BIN points at a local
30+
// build before logicsrc ships a global install.
31+
bin: process.env.LOGICSRC_BIN || "logicsrc",
32+
// LogicSRC ships via its own install script (same pattern as the others).
33+
install: { cmd: "sh", args: ["-c", "curl -fsSL https://logicsrc.com/install.sh | sh"] },
34+
},
35+
};
36+
37+
/** Resolve a name to `[key, tool]`, or null. */
38+
export function resolveTool(token) {
39+
if (!token) return null;
40+
const key = String(token).trim().toLowerCase();
41+
return TOOLS[key] ? [key, TOOLS[key]] : null;
42+
}
43+
44+
/** Tool entries annotated with native executable install status. */
45+
export function toolStatus() {
46+
return Object.entries(TOOLS).map(([key, tool]) => ({
47+
key,
48+
...tool,
49+
installed: isInstalled(tool.bin),
50+
}));
51+
}
52+
53+
export function toolList() {
54+
return Object.entries(TOOLS)
55+
.map(([key, tool]) => ` ${key.padEnd(10)} ${tool.desc}`)
56+
.join("\n");
57+
}
58+
59+
/** Prefer a native updater when one is added; npm installs are idempotent. */
60+
export function toolUpgradeSpec(tool) {
61+
return tool.upgrade || tool.install;
62+
}
63+
64+
/** Invoke a tool without parsing or modifying its arguments or streams. */
65+
export function openTool(tool, args = []) {
66+
return openPassthrough(tool, args);
67+
}
68+
69+
// Generic utilities used by the app/package surface.
270

371
/**
472
* Format a number as currency

test/cli.test.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ test("shell() runs a real command and returns { ok, code }", () => {
164164
const lines = [];
165165
const ctx = { dryRun: false, out: (l) => lines.push(l) };
166166
const cmd = moshVocabulary().get("shell");
167-
const result = cmd.run(ctx, "true");
167+
const result = cmd.run(ctx, "node -e process.exitCode=0");
168168
assert.equal(result.ok, true);
169169
assert.equal(result.code, 0);
170170
});
@@ -173,15 +173,15 @@ test("shell() returns { ok: false } on non-zero exit without throwing", () => {
173173
const lines = [];
174174
const ctx = { dryRun: false, out: (l) => lines.push(l) };
175175
const cmd = moshVocabulary().get("shell");
176-
const result = cmd.run(ctx, "false");
176+
const result = cmd.run(ctx, "node -e process.exitCode=7");
177177
assert.equal(result.ok, false);
178-
assert.ok(result.code !== 0);
178+
assert.equal(result.code, 7);
179179
});
180180

181181
test("shell() is callable from moshscript and the script continues on failure", async () => {
182182
const lines = [];
183183
await runScript(
184-
`const r = shell("false"); say("continued, ok=" + r.ok);`,
184+
`const r = shell("node -e process.exitCode=7"); say("continued, ok=" + r.ok);`,
185185
{ commands: moshVocabulary(), out: (s) => lines.push(s) }
186186
);
187187
assert.match(lines.join("\n"), /continued, ok=false/);

0 commit comments

Comments
 (0)