diff --git a/README.md b/README.md index 3c7aeb5..4e4247a 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,17 @@ Two things stall an agent building on Rome: **hallucinated facts** (wrong ids, a Repo-first (npm publish pending): ```bash -npm install -g github:rome-protocol/rome-cli#v0.8.0 -# or run without installing: -npx github:rome-protocol/rome-cli#v0.8.0 facts chain hadrian +# one-shot, no install: +npx github:rome-protocol/rome-cli facts chain hadrian + +# durable install — clone + link (a plain `npm install -g github:…` fails: npm +# prepares this package's github: dependencies without their own node_modules): +git clone https://github.com/rome-protocol/rome-cli +cd rome-cli && npm install && npm install -g . ``` +Pin a tag (e.g. `github:rome-protocol/rome-cli#v0.8.0`) for reproducible runs. + ## CLI ```bash diff --git a/package-lock.json b/package-lock.json index fc163a9..2bfd92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rome-protocol/cli", - "version": "0.3.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rome-protocol/cli", - "version": "0.3.0", + "version": "0.8.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@rome-protocol/registry": "github:rome-protocol/rome-registry#v0.1.0", @@ -675,9 +675,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -692,9 +689,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -709,9 +703,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -726,9 +717,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -743,9 +731,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -760,9 +745,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -777,9 +759,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -794,9 +773,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -811,9 +787,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -828,9 +801,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -845,9 +815,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -862,9 +829,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -879,9 +843,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1040,7 +1001,6 @@ "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.3.0.tgz", "integrity": "sha512-MuQOCC1j0np1xH9yAv0ZWWfwvr7Bt7Sz4LId11Wi4wDdAmJ+lobE+vHg/mZmGcihF0BIkqVBNxGmlv8QE5DrtA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/web3.js": "^1.32.0", @@ -1056,7 +1016,6 @@ "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", "license": "MIT", - "peer": true, "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", @@ -1073,7 +1032,6 @@ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", "license": "MIT", - "peer": true, "dependencies": { "@solana/errors": "2.0.0-rc.1" }, @@ -1086,7 +1044,6 @@ "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", "license": "MIT", - "peer": true, "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", @@ -1101,7 +1058,6 @@ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", "license": "MIT", - "peer": true, "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" @@ -1115,7 +1071,6 @@ "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", "license": "MIT", - "peer": true, "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", @@ -1131,7 +1086,6 @@ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" @@ -1148,7 +1102,6 @@ "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", "license": "MIT", - "peer": true, "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", @@ -1185,7 +1138,6 @@ "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@solana/codecs": "2.0.0-rc.1" }, @@ -1201,7 +1153,6 @@ "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz", "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@solana/codecs": "2.0.0-rc.1" }, @@ -1217,6 +1168,7 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -1580,7 +1532,6 @@ "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "bindings": "^1.3.0" }, @@ -1593,7 +1544,6 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -1603,7 +1553,6 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", - "peer": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -1695,20 +1644,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/bufferutil": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", - "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1801,7 +1736,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2115,6 +2049,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2219,8 +2154,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler": { "version": "2.1.1", @@ -2363,6 +2297,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -3037,20 +2972,6 @@ "@types/node": "*" } }, - "node_modules/rpc-websockets/node_modules/utf-8-validate": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", - "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/rpc-websockets/node_modules/uuid": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", @@ -3444,6 +3365,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3497,6 +3419,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -3558,6 +3481,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4190,6 +4114,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.3.0" }, @@ -4211,6 +4136,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/cli.ts b/src/cli.ts index e2a3bdf..eab1991 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,6 @@ -import { CAPABILITIES, resolveCli } from "./core/capabilities.js"; +import { CAPABILITIES, resolveCli, type Capability } from "./core/capabilities.js"; import { defaultDeps } from "./core/deps.js"; - -const VERSION = "0.1.0"; +import { PKG_VERSION } from "./core/version.js"; /** Every CLI invocation path the CLI dispatches ("facts chain", "deploy", …). */ export function cliCommandTable(): string[] { @@ -34,6 +33,20 @@ export function parseRest(rest: string[]): { positionals: string[]; flags: Recor return { positionals, flags }; } +/** Per-command usage: `rome --help` — the usage line, summary, and each arg. */ +function capHelpText(cap: Capability): string { + const usage = cap.args.map((x) => (x.required ? `<${x.name}>` : `[${x.name}]`)).join(" "); + const tag = cap.kind === "action" ? " [needs ROME_EVM_KEY]" : ""; + const lines = [`Usage: rome ${cap.cliPath}${usage ? " " + usage : ""}${tag}`, "", ` ${cap.summary}`]; + if (cap.args.length) { + lines.push("", "Args:"); + for (const a of cap.args) { + lines.push(` ${a.name.padEnd(12)} ${a.required ? "(required)" : "(optional)"} ${a.description}`); + } + } + return lines.join("\n"); +} + function helpText(): string { const lines = [ "rome — Rome Protocol dev CLI + MCP server", @@ -60,7 +73,7 @@ export async function main(argv: string[]): Promise { return 0; } if (first === "--version" || first === "-v") { - console.log(VERSION); + console.log(PKG_VERSION); return 0; } if (first === "mcp") { @@ -83,12 +96,24 @@ export async function main(argv: string[]): Promise { // is a boolean flag ("true"). Positionals fill declared args left-to-right; any // extra `--flag` is carried through so handlers can read it. const { positionals, flags } = parseRest(rest); + if (flags.help !== undefined || flags.h !== undefined) { + console.log(capHelpText(cap)); + return 0; + } const argObj: Record = {}; let pi = 0; for (const spec of cap.args) { if (flags[spec.name] !== undefined) argObj[spec.name] = flags[spec.name]; else if (positionals[pi] !== undefined) argObj[spec.name] = positionals[pi++]; } + if (pi < positionals.length) { + console.error(`Unexpected extra argument(s): ${positionals.slice(pi).join(" ")}`); + if (cap.args.some((s) => s.name === "args")) { + console.error(`Function args are passed comma-separated in ONE argument, e.g. "0xRecipient…,42".`); + } + console.error(`See: rome ${cap.cliPath} --help`); + return 1; + } for (const [k, v] of Object.entries(flags)) if (!(k in argObj)) argObj[k] = v; const missing = cap.args.filter((s) => s.required && argObj[s.name] == null); diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index 12ee375..4013d3f 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -92,8 +92,8 @@ export const CAPABILITIES: Capability[] = [ cap( "facts", "programs", - "Solana program ids for a network (devnet | mainnet).", - [{ name: "network", required: true, description: "devnet or mainnet" }], + "Solana program ids for a network (devnet | mainnet; Rome testnet chains settle on Solana devnet).", + [{ name: "network", required: true, description: "devnet or mainnet (testnet settles on the Solana devnet cluster)" }], (a) => getProgramFacts(a.network), ), cap( diff --git a/src/core/facts.ts b/src/core/facts.ts index 56c2b22..b28f265 100644 --- a/src/core/facts.ts +++ b/src/core/facts.ts @@ -73,7 +73,15 @@ export function getProgramFacts(network: string) { if (!KNOWN_NETWORKS.includes(n)) { throw new Error(`Unknown network "${network}". Known networks: ${KNOWN_NETWORKS.join(", ")}.`); } - return { network: n, programs: getPrograms(n) }; + const programs = getPrograms(n) ?? {}; + if (Object.keys(programs).length === 0) { + const note = + n === "testnet" + ? "No Solana programs recorded for testnet — Rome testnet chains settle on the Solana devnet cluster; try `rome facts programs devnet`." + : "No Solana programs recorded for this network."; + return { network: n, programs, note }; + } + return { network: n, programs }; } export async function getGasFacts(input: string | number, deps: Deps = defaultDeps) { diff --git a/src/core/presets.ts b/src/core/presets.ts index d5034e5..d92bdf6 100644 --- a/src/core/presets.ts +++ b/src/core/presets.ts @@ -12,13 +12,18 @@ export interface PresetResult { notes: string[]; } -const romeNotes = (chainId: number): string[] => [ - "Deploy scripts: run `forge script --skip-simulation` — Rome's execution diverges from forge's local simulation. `forge create` and `cast` work as-is.", +// Rome quirks that hold for every toolchain; tool-specific notes are prepended per tool. +const sharedNotes = (chainId: number): string[] => [ "Gas: Rome charges the EXACT gas used; `eth_estimateGas` over-predicts (often 10-50×). Don't hard-fail or size budgets off a high estimate — a native transfer is ~1.48M gas, not 21k.", "App writes go through `submitRomeTx` (@rome-protocol/sdk) for the correct Rome fee path, not a raw signed tx.", `Chain id: ${chainId}.`, ]; +const foundryNotes = (chainId: number): string[] => [ + "Deploy scripts: run `forge script --skip-simulation` — Rome's execution diverges from forge's local simulation. `forge create` and `cast` work as-is.", + ...sharedNotes(chainId), +]; + export function getPreset(tool: string, chain: string | number): PresetResult { const t = tool.trim().toLowerCase(); const c = getChainFacts(chain); @@ -31,7 +36,7 @@ export function getPreset(tool: string, chain: string | number): PresetResult { chainId: c.chainId, filename: "foundry.toml", config: `[rpc_endpoints]\nrome-${slug} = "${c.rpcUrl}"\n\n# deploy: forge create --rpc-url rome-${slug} ... (scripts: add --skip-simulation)`, - notes: romeNotes(c.chainId), + notes: foundryNotes(c.chainId), }; } if (t === "hardhat") { @@ -40,7 +45,7 @@ export function getPreset(tool: string, chain: string | number): PresetResult { chainId: c.chainId, filename: "hardhat.config.ts", config: `networks: {\n ${camel}: {\n url: "${c.rpcUrl}",\n chainId: ${c.chainId},\n },\n}`, - notes: romeNotes(c.chainId), + notes: sharedNotes(c.chainId), }; } throw new Error(`Unknown preset tool "${tool}". Supported: foundry, hardhat.`); diff --git a/src/core/version.ts b/src/core/version.ts new file mode 100644 index 0000000..a91d151 --- /dev/null +++ b/src/core/version.ts @@ -0,0 +1,5 @@ +import { createRequire } from "node:module"; + +// Single source of the released version — package.json — so the CLI banner and +// the MCP serverInfo can't drift from the tag. +export const PKG_VERSION: string = createRequire(import.meta.url)("../../package.json").version; diff --git a/src/mcp.ts b/src/mcp.ts index d9a4e77..a0a7a33 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z, type ZodRawShape } from "zod"; import { CAPABILITIES, type Capability } from "./core/capabilities.js"; import { defaultDeps } from "./core/deps.js"; +import { PKG_VERSION } from "./core/version.js"; export interface McpToolDef { name: string; @@ -29,7 +30,7 @@ function inputShape(cap: Capability): ZodRawShape { /** Start the read-only MCP server over stdio. Holds no keys; every tool is a read. */ export async function startMcpServer(): Promise { - const server = new McpServer({ name: "rome-mcp", version: "0.1.0" }); + const server = new McpServer({ name: "rome-mcp", version: PKG_VERSION }); for (const { name, description, capability } of buildMcpTools()) { server.registerTool(name, { description, inputSchema: inputShape(capability) }, async (args: Record) => { const strArgs: Record = {}; diff --git a/test/cli.test.ts b/test/cli.test.ts index 637004a..1af8a0e 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, afterEach } from "vitest"; import { main, parseRest } from "../src/cli.js"; +import { readFileSync } from "node:fs"; // Drive the REAL CLI dispatch (findCapability + positional-arg mapping + exit codes), // not just the capability table — this is the behavioral half of the alignment gate. @@ -102,3 +103,47 @@ describe("parseRest (positionals + --flags)", () => { }); }); }); + +describe("CLI guardrails — extra positionals, per-command help, version", () => { + it("errors (exit 1) on extra positional args instead of silently dropping them", async () => { + const c = capture(); + const code = await main([ + "node", "rome", "call", "hadrian", + "0x0000000000000000000000000000000000000001", + "allowance(address,address) returns (uint256)", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + ]); + c.restore(); + expect(code).toBe(1); + expect(c.err.join("\n")).toMatch(/unexpected extra argument/i); + expect(c.err.join("\n")).toMatch(/comma-separated/i); + }); + + it("prints per-command usage on `rome call --help` and exits 0", async () => { + const c = capture(); + const code = await main(["node", "rome", "call", "--help"]); + c.restore(); + expect(code).toBe(0); + const out = c.out.join("\n"); + expect(out).toMatch(/rome call
\[args\]/); + expect(out).toMatch(/comma-separated/i); + }); + + it("prints per-command usage for a two-word command (`rome facts chain --help`)", async () => { + const c = capture(); + const code = await main(["node", "rome", "facts", "chain", "--help"]); + c.restore(); + expect(code).toBe(0); + expect(c.out.join("\n")).toMatch(/rome facts chain /); + }); + + it("--version prints the real package version, not a hardcoded string", async () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + const c = capture(); + const code = await main(["node", "rome", "--version"]); + c.restore(); + expect(code).toBe(0); + expect(c.out.join("\n").trim()).toBe(pkg.version); + }); +}); diff --git a/test/facts.test.ts b/test/facts.test.ts index 928adcc..b84707f 100644 --- a/test/facts.test.ts +++ b/test/facts.test.ts @@ -90,3 +90,17 @@ describe("getGasFacts + getBalanceFacts (RPC via injected deps)", () => { expect(r.balanceWei).toBe("5000000000000000000"); }); }); + +describe("getProgramFacts — network edge cases", () => { + it("testnet: empty program set carries a note pointing at the Solana devnet cluster", () => { + const r = getProgramFacts("testnet"); + expect(r.programs).toEqual({}); + expect(r.note).toMatch(/devnet/); + }); + + it("devnet: populated program set has no note", () => { + const r = getProgramFacts("devnet"); + expect(Object.keys(r.programs).length).toBeGreaterThan(0); + expect(r.note).toBeUndefined(); + }); +}); diff --git a/test/presets.test.ts b/test/presets.test.ts index 5347ad6..aa3f900 100644 --- a/test/presets.test.ts +++ b/test/presets.test.ts @@ -40,3 +40,17 @@ describe("preset is a read capability on MCP", () => { expect(new Set(buildMcpTools().map((t) => t.name)).has("preset")).toBe(true); }); }); + +describe("preset notes are tool-specific", () => { + it("hardhat: no forge-only guidance leaks into the hardhat notes", () => { + const p = getPreset("hardhat", "hadrian"); + const joined = p.notes.join(" "); + expect(joined).not.toMatch(/forge/i); + expect(joined).toMatch(/exact gas/i); + }); + + it("foundry: keeps the forge skip-simulation quirk", () => { + const p = getPreset("foundry", "hadrian"); + expect(p.notes.join(" ")).toMatch(/forge script --skip-simulation/); + }); +}); diff --git a/test/version.test.ts b/test/version.test.ts new file mode 100644 index 0000000..ff22218 --- /dev/null +++ b/test/version.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { PKG_VERSION } from "../src/core/version.js"; + +// One version, sourced from package.json — the CLI banner and the MCP serverInfo +// must never drift from the released package version again. + +describe("PKG_VERSION", () => { + it("matches package.json version", () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + expect(PKG_VERSION).toBe(pkg.version); + }); + + it("is consumed by the MCP server module (no hardcoded serverInfo version)", () => { + const mcpSrc = readFileSync(new URL("../src/mcp.ts", import.meta.url), "utf8"); + expect(mcpSrc).toContain("PKG_VERSION"); + expect(mcpSrc).not.toMatch(/version:\s*"0\.1\.0"/); + }); +});