diff --git a/README.md b/README.md index a128e55..5a68106 100644 --- a/README.md +++ b/README.md @@ -869,6 +869,30 @@ engines, `mcpjam` tells you whether that server is healthy first — health checks, OAuth conformance, tool-surface diffing, and structured triage from the terminal or CI. Re-running `moshcode install mcpjam` is also its upgrade path. +### Noodle — a REST client that lives in the repo + +```sh +moshcode install noodle # static binary → ~/.local/bin + +moshcode noodle collection create my-api +moshcode noodle request create users/get --url https://api.example.com/users/42 +moshcode noodle request run users/get --collection ./my-api +moshcode noodle --collection ./my-api # the TUI +``` + +[Noodle](https://noodlerest.dev/) is MCPJam one protocol over: MCPJam tells you +whether an MCP server answers, Noodle is how you ask an HTTP one anything at +all. Every request is a readable YAML file — method, url, path params, headers +— so a collection is reviewed in a diff and kept beside the code it exercises, +rather than living in a desktop app's private workspace. The same files drive +the TUI, the CLI and a script, which is what makes it a roster tool: `request +run` exits with a status and prints the exchange, so it pipes. + +It ships a per-platform static binary on its own releases and the install script +picks the right one, checks it against the published `SHA256SUMS`, and renames +it over any existing copy. So there is no updater to reach for: re-running +`moshcode install noodle` is the upgrade path. + ### ElevenLabs — Eleven Agents, voices, and speech ```sh diff --git a/src/commands.mjs b/src/commands.mjs index 026762f..e495145 100644 --- a/src/commands.mjs +++ b/src/commands.mjs @@ -853,6 +853,7 @@ const COMMANDS = [ cliVerb("coral", "drive the Coral CLI (SQL over APIs, databases, and internal systems)"), cliVerb("alpaca", "drive the native Alpaca trading CLI"), cliVerb("mcpjam", "drive the MCPJam CLI (test, debug, and validate MCP servers)"), + cliVerb("noodle", "drive the Noodle CLI (terminal REST client: collections, requests, runs)"), cliVerb("spinifex", "drive the Spinifex CLI (spx — AWS-compatible cloud on your own hardware)"), cliVerb("alchemy", "drive the Alchemy CLI (onchain data, apps, wallets, x402)"), cliVerb("elevenlabs", "drive the ElevenLabs CLI (Eleven Agents, voices, TTS, dubbing)"), diff --git a/src/tools.mjs b/src/tools.mjs index 28ed70f..fec1e21 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -304,6 +304,32 @@ export const TOOLS = { // idempotent, so it doubles as the upgrade path — no `upgrade` key needed. install: { cmd: "npm", args: ["install", "-g", "@mcpjam/cli"] }, }, + noodle: { + desc: "Noodle — terminal REST client: readable request files, run them from the TUI, CLI or a script", + bin: "noodle", + // The sibling of mcpjam one protocol over: mcpjam tells you whether an MCP + // server answers, noodle is how you ask an HTTP one anything at all. Every + // request is a YAML file that lives beside the code it exercises, so a + // collection is reviewable in a diff and runnable from a pipeline rather + // than trapped in a desktop app's workspace. + // + // Its own install script, not npm: noodle ships a per-platform static + // binary on its GitHub releases and the script picks the right one. The + // shebang is bash and the script uses `local`, so it goes through bash + // rather than `sh` — the opposite of the c0upons entry above, which avoids + // bashisms on purpose. + install: { cmd: "bash", args: ["-c", "curl -fsSL https://noodlerest.dev/install.sh | bash"] }, + // No `upgrade` key on purpose: noodle ships no updater of its own, and the + // installer always fetches the latest release, checks it against the + // published SHA256SUMS, and renames it over the existing binary atomically. + // Re-running the install IS the upgrade, the same as mcpjam and railway, + // and toolUpgradeSpec falls back to install on its own. + // + // It lands in ~/.local/bin and appends nothing to PATH, so the shell that + // ran the install cannot see it — the same gap turso, gradient, kimi and + // yt-dlp have. + binDirs: [path.join(homedir(), ".local", "bin")], + }, spinifex: { desc: "Spinifex — AWS-compatible cloud on your own hardware (EC2, EBS, S3, VPC, IAM)", // The product is Spinifex; the binary it installs is `spx`. Same split as diff --git a/test/tools.test.mjs b/test/tools.test.mjs index 8f70728..2332d4a 100644 --- a/test/tools.test.mjs +++ b/test/tools.test.mjs @@ -523,6 +523,7 @@ for (const [name, shell, script] of [ ["c0mpute", "sh", "curl -fsSL https://c0mpute.com/install.sh | sh"], ["c0upons", "sh", "curl -fsSL https://c0upons.com/install.sh | sh"], ["coral", "bash", "curl -fsSL https://withcoral.com/install.sh | bash"], + ["noodle", "bash", "curl -fsSL https://noodlerest.dev/install.sh | bash"], ["spinifex", "bash", "curl -fsSL https://install.mulgadc.com | INSTALL_SPINIFEX_SKIP_NEWGRP=1 bash"], [ "cli-tools", @@ -606,6 +607,25 @@ test("the media toolchain installs through the right mechanism for each tool", ( } }); +test("noodle installs through its own script and re-runs it to upgrade", () => { + assert.deepEqual(resolveTool("noodle"), ["noodle", TOOLS.noodle]); + assert.equal(TOOLS.noodle.bin, "noodle"); + assert.match(toolList(), /noodle/); + // bash, not sh: noodle's installer declares a bash shebang and uses `local`, + // unlike the c0upons script next to it in the registry. + assert.deepEqual(TOOLS.noodle.install, { + cmd: "bash", + args: ["-c", "curl -fsSL https://noodlerest.dev/install.sh | bash"], + }); + // No updater of its own, and the installer replaces the binary in place from + // the latest release, so the upgrade is the install. + assert.equal(toolUpgradeSpec(TOOLS.noodle), TOOLS.noodle.install); + // A user-local binary in ~/.local/bin, so it never escalates, and the shell + // that ran the install needs the directory named to find it afterwards. + assert.equal(needsRootHere(TOOLS.noodle, "linux"), false); + assert.deepEqual(TOOLS.noodle.binDirs, [path.join(homedir(), ".local", "bin")]); +}); + test("the package-manager tools ask for root everywhere but macOS", () => { // Homebrew refuses to run as root, so a mac must not be prompted for a // password by a step that never escalates.