From 67323811ea1a72d97f95db41b61fa7268a852285 Mon Sep 17 00:00:00 2001 From: Petar Stoev Date: Wed, 8 Apr 2026 11:12:53 +0300 Subject: [PATCH 1/3] fix: use legacy-peer-deps in CI to resolve eslint version conflict --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50f44c..dc18f85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: with: node-version: 20 cache: npm - - run: npm ci + - run: npm ci --legacy-peer-deps - run: npm run typecheck - run: npm run lint - run: npm run format:check From 1984f2892929418fe21ad094bdcaeadd714ca78e Mon Sep 17 00:00:00 2001 From: Petar Stoev Date: Wed, 8 Apr 2026 11:14:36 +0300 Subject: [PATCH 2/3] fix: resolve lint and formatting errors --- src/commands/add.ts | 34 +++++++++++++----- src/commands/init.ts | 77 +++++++++++++++++++++++++++++++--------- src/commands/registry.ts | 73 ++++++++++++++++++++++++++++--------- src/utils/deps.ts | 14 +++++--- src/utils/display.ts | 8 ++--- src/utils/github.ts | 15 ++++---- 6 files changed, 161 insertions(+), 60 deletions(-) diff --git a/src/commands/add.ts b/src/commands/add.ts index 247418e..9532851 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -15,7 +15,7 @@ export function parseRecipeMeta(json: string): RecipeMeta { export function resolveRecipeFiles( files: { path: string; content: string }[], - chain: ChainFilter + chain: ChainFilter, ): { path: string; content: string }[] { return files.filter((f) => { const name = basename(f.path); @@ -51,7 +51,7 @@ export function createAddCommand(): Command { async function addRecipe( name: string | undefined, - options: { chain?: string; learn: boolean } + options: { chain?: string; learn: boolean }, ): Promise { if (!name) { const spinner = ora("Fetching available recipes...").start(); @@ -71,7 +71,10 @@ async function addRecipe( choices: recipes.map((r) => ({ title: r, value: r })), }); - if (!response.recipe) { console.log("Cancelled."); return; } + if (!response.recipe) { + console.log("Cancelled."); + return; + } name = response.recipe as string; } @@ -88,7 +91,10 @@ async function addRecipe( ], }); chain = response.chain; - if (!chain) { console.log("Cancelled."); return; } + if (!chain) { + console.log("Cancelled."); + return; + } } const spinner = ora(`Downloading ${name}...`).start(); @@ -116,8 +122,10 @@ async function addRecipe( try { const meta = parseRecipeMeta(metaFile.content); const deps: string[] = []; - if ((chain === "evm" || chain === "both") && meta.dependencies.evm) deps.push(...meta.dependencies.evm); - if ((chain === "solana" || chain === "both") && meta.dependencies.solana) deps.push(...meta.dependencies.solana); + if ((chain === "evm" || chain === "both") && meta.dependencies.evm) + deps.push(...meta.dependencies.evm); + if ((chain === "solana" || chain === "both") && meta.dependencies.solana) + deps.push(...meta.dependencies.solana); if (deps.length > 0) { const depSpinner = ora(`Installing dependencies (${deps.join(", ")})...`).start(); @@ -128,7 +136,9 @@ async function addRecipe( depSpinner.fail(`Failed to install. Run manually: npm install ${deps.join(" ")}`); } } - } catch { /* No valid meta.json — skip deps */ } + } catch { + /* No valid meta.json — skip deps */ + } } if (options.learn) { @@ -150,7 +160,10 @@ async function addContract(name: string | undefined): Promise { process.exit(1); } - if (contracts.length === 0) { console.log("No contract templates available yet."); return; } + if (contracts.length === 0) { + console.log("No contract templates available yet."); + return; + } const response = await prompts({ type: "autocomplete", @@ -159,7 +172,10 @@ async function addContract(name: string | undefined): Promise { choices: contracts.map((c) => ({ title: c, value: c })), }); - if (!response.contract) { console.log("Cancelled."); return; } + if (!response.contract) { + console.log("Cancelled."); + return; + } name = response.contract as string; } diff --git a/src/commands/init.ts b/src/commands/init.ts index 43ba243..e70e480 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -9,16 +9,33 @@ import { detectPackageManager, runInstall } from "../utils/deps.js"; import type { ChainFilter, TemplateMeta } from "../types.js"; const TEMPLATES: TemplateMeta[] = [ - { name: "nextjs-evm", description: "Next.js dApp with wagmi + viem + RainbowKit", chain: "evm", stack: "Next.js + wagmi + viem + RainbowKit" }, - { name: "script-evm", description: "TypeScript script with viem", chain: "evm", stack: "TypeScript + viem" }, - { name: "nextjs-solana", description: "Next.js dApp with wallet-adapter", chain: "solana", stack: "Next.js + wallet-adapter" }, - { name: "script-solana", description: "TypeScript script with @solana/web3.js", chain: "solana", stack: "TypeScript + @solana/web3.js" }, + { + name: "nextjs-evm", + description: "Next.js dApp with wagmi + viem + RainbowKit", + chain: "evm", + stack: "Next.js + wagmi + viem + RainbowKit", + }, + { + name: "script-evm", + description: "TypeScript script with viem", + chain: "evm", + stack: "TypeScript + viem", + }, + { + name: "nextjs-solana", + description: "Next.js dApp with wallet-adapter", + chain: "solana", + stack: "Next.js + wallet-adapter", + }, + { + name: "script-solana", + description: "TypeScript script with @solana/web3.js", + chain: "solana", + stack: "TypeScript + @solana/web3.js", + }, ]; -export function filterTemplates( - templates: TemplateMeta[], - chain: ChainFilter -): TemplateMeta[] { +export function filterTemplates(templates: TemplateMeta[], chain: ChainFilter): TemplateMeta[] { if (chain === "both") return templates; return templates.filter((t) => t.chain === chain); } @@ -38,12 +55,20 @@ export function createInitCommand(): Command { async function runInit( name: string | undefined, - options: { template?: string; chain?: string; pm?: string } + options: { template?: string; chain?: string; pm?: string }, ): Promise { if (!name) { - const response = await prompts({ type: "text", name: "name", message: "Project name:", initial: "my-dapp" }); + const response = await prompts({ + type: "text", + name: "name", + message: "Project name:", + initial: "my-dapp", + }); name = response.name; - if (!name) { console.log("Cancelled."); return; } + if (!name) { + console.log("Cancelled."); + return; + } } const projectDir = join(process.cwd(), name); @@ -55,7 +80,9 @@ async function runInit( let chain = options.chain as ChainFilter | undefined; if (!chain) { const response = await prompts({ - type: "select", name: "chain", message: "Which chain?", + type: "select", + name: "chain", + message: "Which chain?", choices: [ { title: "EVM (Ethereum, Arbitrum, Base, Polygon...)", value: "evm" }, { title: "Solana", value: "solana" }, @@ -63,18 +90,26 @@ async function runInit( ], }); chain = response.chain; - if (!chain) { console.log("Cancelled."); return; } + if (!chain) { + console.log("Cancelled."); + return; + } } let templateName = options.template; if (!templateName) { const available = filterTemplates(TEMPLATES, chain); const response = await prompts({ - type: "select", name: "template", message: "Pick a template:", + type: "select", + name: "template", + message: "Pick a template:", choices: available.map((t) => ({ title: `${t.name} — ${t.description}`, value: t.name })), }); templateName = response.template; - if (!templateName) { console.log("Cancelled."); return; } + if (!templateName) { + console.log("Cancelled."); + return; + } } const spinner = ora("Fetching template...").start(); @@ -90,10 +125,18 @@ async function runInit( const prefix = `templates/${templateName}/`; for (const file of files) { - const relativePath = file.path.startsWith(prefix) ? file.path.slice(prefix.length) : basename(file.path); + const relativePath = file.path.startsWith(prefix) + ? file.path.slice(prefix.length) + : basename(file.path); let content = file.content; if (relativePath === "package.json") { - try { const pkg = JSON.parse(content); pkg.name = name; content = JSON.stringify(pkg, null, 2) + "\n"; } catch {} + try { + const pkg = JSON.parse(content); + pkg.name = name; + content = JSON.stringify(pkg, null, 2) + "\n"; + } catch { + // ignore invalid JSON + } } const destPath = join(projectDir, relativePath); mkdirSync(dirname(destPath), { recursive: true }); diff --git a/src/commands/registry.ts b/src/commands/registry.ts index d33dd78..fb9a5a4 100644 --- a/src/commands/registry.ts +++ b/src/commands/registry.ts @@ -5,7 +5,12 @@ import { formatTable, formatKeyValue, formatJson } from "../utils/display.js"; export function formatChainsTable(chains: Chain[]) { const headers = ["Chain", "Chain ID", "Type", "Native Token"]; - const rows = chains.map((c) => [c.name, String(c.chainId), c.testnet ? "testnet" : c.ecosystem, c.nativeCurrency.symbol]); + const rows = chains.map((c) => [ + c.name, + String(c.chainId), + c.testnet ? "testnet" : c.ecosystem, + c.nativeCurrency.symbol, + ]); return { headers, rows }; } @@ -14,7 +19,10 @@ export function formatChainDetail(chain: Chain): [string, string][] { ["Chain", chain.name], ["Chain ID", String(chain.chainId)], ["Ecosystem", chain.ecosystem], - ["Native Token", `${chain.nativeCurrency.name} (${chain.nativeCurrency.symbol}, ${chain.nativeCurrency.decimals} decimals)`], + [ + "Native Token", + `${chain.nativeCurrency.name} (${chain.nativeCurrency.symbol}, ${chain.nativeCurrency.decimals} decimals)`, + ], ["RPC", chain.rpcUrls[0] || "—"], ["Explorer", chain.blockExplorers[0] || "—"], ["Testnet", chain.testnet ? "Yes" : "No"], @@ -25,45 +33,76 @@ export function formatTokenTable(token: Token) { const chains = getAllChains(); const chainMap = new Map(chains.map((c) => [c.chainId, c.name])); const headers = ["Chain", "Address", "Decimals"]; - const rows = token.chains.map((c) => [chainMap.get(c.chainId) || String(c.chainId), c.address, String(token.decimals)]); + const rows = token.chains.map((c) => [ + chainMap.get(c.chainId) || String(c.chainId), + c.address, + String(token.decimals), + ]); return { headers, rows }; } export function createRegistryCommand(): Command { const registry = new Command("registry").description("Query chain and token data"); - registry.command("chains").description("List all supported chains").option("--json", "Output as JSON") + registry + .command("chains") + .description("List all supported chains") + .option("--json", "Output as JSON") .action((options) => { const chains = getAllChains(); - if (options.json) { console.log(formatJson(chains)); } - else { const { headers, rows } = formatChainsTable(chains); console.log(formatTable(headers, rows)); } + if (options.json) { + console.log(formatJson(chains)); + } else { + const { headers, rows } = formatChainsTable(chains); + console.log(formatTable(headers, rows)); + } }); - registry.command("chain ").description("Show details for a specific chain").option("--json", "Output as JSON") + registry + .command("chain ") + .description("Show details for a specific chain") + .option("--json", "Output as JSON") .action((id, options) => { const chain = getChain(Number(id)); - if (!chain) { console.error(`Chain with ID ${id} not found.`); process.exit(1); } - if (options.json) { console.log(formatJson(chain)); } - else { console.log(formatKeyValue(formatChainDetail(chain))); } + if (!chain) { + console.error(`Chain with ID ${id} not found.`); + process.exit(1); + } + if (options.json) { + console.log(formatJson(chain)); + } else { + console.log(formatKeyValue(formatChainDetail(chain))); + } }); - registry.command("tokens").description("List all supported tokens").option("--json", "Output as JSON") + registry + .command("tokens") + .description("List all supported tokens") + .option("--json", "Output as JSON") .action((options) => { const tokens = getAllTokens(); - if (options.json) { console.log(formatJson(tokens)); } - else { + if (options.json) { + console.log(formatJson(tokens)); + } else { const headers = ["Token", "Symbol", "Chains"]; const rows = tokens.map((t) => [t.name, t.symbol, String(t.chains.length)]); console.log(formatTable(headers, rows)); } }); - registry.command("token ").description("Show token addresses across chains").option("--json", "Output as JSON") + registry + .command("token ") + .description("Show token addresses across chains") + .option("--json", "Output as JSON") .action((symbol, options) => { const token = getToken(symbol); - if (!token) { console.error(`Token "${symbol}" not found.`); process.exit(1); } - if (options.json) { console.log(formatJson(token)); } - else { + if (!token) { + console.error(`Token "${symbol}" not found.`); + process.exit(1); + } + if (options.json) { + console.log(formatJson(token)); + } else { const { headers, rows } = formatTokenTable(token); console.log(`\n${token.name} (${token.symbol})\n`); console.log(formatTable(headers, rows)); diff --git a/src/utils/deps.ts b/src/utils/deps.ts index 3603468..bc710fb 100644 --- a/src/utils/deps.ts +++ b/src/utils/deps.ts @@ -13,14 +13,18 @@ export function detectPackageManager(projectDir: string): PackageManager { export function buildInstallCommand( pm: PackageManager, - packages: string[] + packages: string[], ): { command: string; args: string[] } | null { if (packages.length === 0) return null; switch (pm) { - case "npm": return { command: "npm", args: ["install", ...packages] }; - case "pnpm": return { command: "pnpm", args: ["add", ...packages] }; - case "yarn": return { command: "yarn", args: ["add", ...packages] }; - case "bun": return { command: "bun", args: ["add", ...packages] }; + case "npm": + return { command: "npm", args: ["install", ...packages] }; + case "pnpm": + return { command: "pnpm", args: ["add", ...packages] }; + case "yarn": + return { command: "yarn", args: ["add", ...packages] }; + case "bun": + return { command: "bun", args: ["add", ...packages] }; } } diff --git a/src/utils/display.ts b/src/utils/display.ts index 35938f4..366f420 100644 --- a/src/utils/display.ts +++ b/src/utils/display.ts @@ -2,9 +2,7 @@ import chalk from "chalk"; export function formatTable(headers: string[], rows: string[][]): string { const allRows = [headers, ...rows]; - const colWidths = headers.map((_, i) => - Math.max(...allRows.map((row) => (row[i] || "").length)) - ); + const colWidths = headers.map((_, i) => Math.max(...allRows.map((row) => (row[i] || "").length))); const topBorder = "┌" + colWidths.map((w) => "─".repeat(w + 2)).join("┬") + "┐"; const midBorder = "├" + colWidths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"; @@ -26,9 +24,7 @@ export function formatTable(headers: string[], rows: string[][]): string { export function formatKeyValue(pairs: [string, string][]): string { const maxKeyLen = Math.max(...pairs.map(([k]) => k.length)); - return pairs - .map(([key, value]) => `${chalk.bold(key.padEnd(maxKeyLen))} ${value}`) - .join("\n"); + return pairs.map(([key, value]) => `${chalk.bold(key.padEnd(maxKeyLen))} ${value}`).join("\n"); } export function formatJson(data: unknown): string { diff --git a/src/utils/github.ts b/src/utils/github.ts index 4ae51cf..44d1eb9 100644 --- a/src/utils/github.ts +++ b/src/utils/github.ts @@ -30,21 +30,24 @@ export async function fetchGithubFile(downloadUrl: string): Promise { return response.text(); } -export async function fetchDirectoryRecursive(repo: string, path: string): Promise<{ path: string; content: string }[]> { +export async function fetchDirectoryRecursive( + repo: string, + path: string, +): Promise<{ path: string; content: string }[]> { const entries = await fetchGithubDirectory(repo, path); const filePromises = entries - .filter((e): e is GithubFile & { download_url: string } => e.type === "file" && e.download_url !== null) + .filter( + (e): e is GithubFile & { download_url: string } => + e.type === "file" && e.download_url !== null, + ) .map((e) => fetchGithubFile(e.download_url).then((content) => ({ path: e.path, content }))); const dirPromises = entries .filter((e) => e.type === "dir") .map((e) => fetchDirectoryRecursive(repo, e.path)); - const [files, ...nestedArrays] = await Promise.all([ - Promise.all(filePromises), - ...dirPromises, - ]); + const [files, ...nestedArrays] = await Promise.all([Promise.all(filePromises), ...dirPromises]); return [...files, ...nestedArrays.flat()]; } From 9e9787644a2baa1504afd7853b603f9a902ccc92 Mon Sep 17 00:00:00 2001 From: Petar Stoev Date: Wed, 8 Apr 2026 11:21:41 +0300 Subject: [PATCH 3/3] fix: add .npmrc with legacy-peer-deps for consistent installs --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true