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
107 changes: 51 additions & 56 deletions src/tools.mjs
Original file line number Diff line number Diff line change
@@ -1,67 +1,62 @@
// 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";
// tools.mjs - Moshcode utility functions

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"] },
},
};
/**
* Format a number as currency
*/
export function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}

/** 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;
/**
* Generate a random ID
*/
export function generateId(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}

/** Tool entries annotated with native executable install status. */
export function toolStatus() {
return Object.entries(TOOLS).map(([key, tool]) => ({
key,
...tool,
installed: isInstalled(tool.bin),
}));
/**
* Debounce function calls
*/
export function debounce(fn, delay = 300) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}

export function toolList() {
return Object.entries(TOOLS)
.map(([key, tool]) => ` ${key.padEnd(10)} ${tool.desc}`)
.join("\n");
/**
* Deep clone an object
*/
export function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}

/** Prefer a native updater when one is added; npm installs are idempotent. */
export function toolUpgradeSpec(tool) {
return tool.upgrade || tool.install;
/**
* Sleep for ms milliseconds
*/
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

/** Invoke a tool without parsing or modifying its arguments or streams. */
export function openTool(tool, args = []) {
return openPassthrough(tool, args);
/**
* Retry a function with exponential backoff
*/
export async function retry(fn, maxAttempts = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
await sleep(baseDelay * Math.pow(2, attempt - 1));
}
}
}
84 changes: 84 additions & 0 deletions tests/tools.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { formatCurrency, generateId, debounce, deepClone, sleep, retry } from '../src/tools.mjs';

describe('formatCurrency', () => {
it('formats USD by default', () => {
assert.equal(formatCurrency(1234.56), '$1,234.56');
});

it('formats EUR', () => {
assert.equal(formatCurrency(100, 'EUR'), '€100.00');
});

it('formats zero', () => {
assert.equal(formatCurrency(0), '$0.00');
});
});

describe('generateId', () => {
it('generates id of specified length', () => {
assert.equal(generateId(8).length, 8);
assert.equal(generateId(16).length, 16);
});

it('generates different ids', () => {
const id1 = generateId();
const id2 = generateId();
assert.notEqual(id1, id2);
});

it('only contains alphanumeric chars', () => {
assert.match(generateId(100), /^[a-z0-9]+$/);
});
});

describe('debounce', () => {
it('delays function execution', async () => {
let called = false;
const fn = () => { called = true; };
const debounced = debounce(fn, 50);
debounced();
assert.equal(called, false);
await sleep(60);
assert.equal(called, true);
});
});

describe('deepClone', () => {
it('creates a deep copy', () => {
const original = { a: 1, b: { c: 2 } };
const clone = deepClone(original);
clone.b.c = 3;
assert.equal(original.b.c, 2);
assert.equal(clone.b.c, 3);
});
});

describe('sleep', () => {
it('resolves after delay', async () => {
const start = Date.now();
await sleep(50);
const elapsed = Date.now() - start;
assert.ok(elapsed >= 40); // Allow some timing tolerance
});
});

describe('retry', () => {
it('retries on failure', async () => {
let attempts = 0;
const fn = async () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retry(fn, 3, 10);
assert.equal(result, 'success');
assert.equal(attempts, 3);
});

it('throws after max attempts', async () => {
const fn = async () => { throw new Error('always fail'); };
await assert.rejects(() => retry(fn, 2, 10), /always fail/);
});
});
Loading