diff --git a/.changeset/tiny-animals-agree.md b/.changeset/tiny-animals-agree.md new file mode 100644 index 00000000..7a71a1f3 --- /dev/null +++ b/.changeset/tiny-animals-agree.md @@ -0,0 +1,6 @@ +--- +"@perfect-abstractions/compose-cli": patch +--- + +add rpc adapter using viem, add diamond inspect command querying deployed diamonds + \ No newline at end of file diff --git a/cli/package.json b/cli/package.json index 43de8287..a8bba39c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -36,6 +36,7 @@ "@inquirer/select": "5.2.1", "@perfect-abstractions/compose": "0.0.4", "commander": "^13.1.0", + "dotenv": "^16.4.7", "fs-extra": "^11.3.3", "picocolors": "^1.1.1", "viem": "^2.52.2" diff --git a/cli/src/adapters/interface/IRPCAdapter.ts b/cli/src/adapters/interface/IRPCAdapter.ts new file mode 100644 index 00000000..41f14625 --- /dev/null +++ b/cli/src/adapters/interface/IRPCAdapter.ts @@ -0,0 +1,20 @@ +import type { Address, Abi, Hex } from "viem"; +import type { ReadContractParameters } from "viem"; + +/** Optional behavior for a contract read. */ +export type RPCReadContractOptions = { + /** Verify that bytecode exists at the target before reading. */ + verifyCode?: boolean; +}; + +/** Generic read-only RPC boundary used by CLI modules. */ +export interface IRPCAdapter { + /** Read a view or pure contract function and return its decoded value. */ + readContract( + parameters: ReadContractParameters, + options?: RPCReadContractOptions, + ): Promise; + + /** Return deployed bytecode, or undefined when the account has no code. */ + getCode(address: Address): Promise; +} diff --git a/cli/src/adapters/rpc/adapter.ts b/cli/src/adapters/rpc/adapter.ts new file mode 100644 index 00000000..58873a40 --- /dev/null +++ b/cli/src/adapters/rpc/adapter.ts @@ -0,0 +1,111 @@ +import { + createPublicClient, + defineChain, + getAddress, + http, + type Address, + type Chain, + type Hex, + type ReadContractParameters, +} from "viem"; +import type { IRPCAdapter, RPCReadContractOptions } from "../interface/IRPCAdapter"; +import { requestError, RPCAdapterError } from "./errors"; +import { retryRPC } from "./retry"; +import type { RPCAdapterOptions } from "./types"; +import { validateOptions } from "./validation"; + +/** + * Creates the minimal viem chain definition needed by a custom RPC endpoint. + * @param chainId Expected EVM chain ID. + * @param rpcUrl HTTP or HTTPS JSON-RPC endpoint. + * @returns A viem chain definition bound to the endpoint. + */ +function chainFor(chainId: number, rpcUrl: string): Chain { + return defineChain({ + id: chainId, + name: `Compose chain ${chainId}`, + nativeCurrency: { name: "Native", symbol: "NATIVE", decimals: 18 }, + rpcUrls: { default: { http: [rpcUrl] } }, + }); +} + +/** + * Creates an isolated, chain-specific read-only RPC adapter. + * + * Construction performs a chain-ID probe, so a successfully created adapter + * has already verified that its endpoint points at the requested network. + * Requests use the adapter retry policy rather than viem transport retries. + * @param options RPC endpoint and expected chain configuration. + * @returns A configured read-only RPC adapter. + * @throws {RPCAdapterError} If configuration is invalid, the endpoint is + * unreachable, unauthorized, or reports a different chain ID. + */ +export async function createRPCAdapter(options: RPCAdapterOptions): Promise { + validateOptions(options); + const chain = chainFor(options.chainId, options.rpcUrl); + const client = createPublicClient({ chain, transport: http(options.rpcUrl, { retryCount: 0 }) }); + + let endpointChainId: number; + try { + endpointChainId = await retryRPC(() => client.getChainId()); + } catch (error) { + throw error instanceof RPCAdapterError ? error : requestError("getChainId", options.chainId, error); + } + if (endpointChainId !== options.chainId) { + throw new RPCAdapterError( + "RPC_CHAIN_ID_MISMATCH", + `RPC endpoint reports chain ${endpointChainId}, expected chain ${options.chainId}`, + { operation: "getChainId", chainId: options.chainId }, + ); + } + + /** + * Returns deployed bytecode at an address, or no code for an EOA/empty account. + * @param address Address to inspect. + * @returns Deployed bytecode, or `undefined` when no bytecode exists. + * @throws {RPCAdapterError} If the RPC request fails. + */ + async function getCode(address: Address): Promise { + try { + return await retryRPC(() => client.getCode({ address })); + } catch (error) { + throw requestError("getCode", options.chainId, error); + } + } + + /** + * Reads and decodes a view/pure contract function through the configured RPC. + * @param parameters Contract address, ABI, function name, and arguments. + * @param readOptions Optional contract-code verification settings. + * @returns The decoded contract result. + * @throws {RPCAdapterError} If verification or the RPC request fails. + */ + async function readContract(parameters: ReadContractParameters, readOptions?: RPCReadContractOptions): Promise { + try { + if (readOptions?.verifyCode) { + if (!parameters.address) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "Contract address is required when verifyCode is enabled", { + operation: "readContract", + chainId: options.chainId, + }); + } + const code = await getCode(getAddress(parameters.address)); + if (!code || code === "0x") { + throw new RPCAdapterError("RPC_CONTRACT_NOT_FOUND", `No contract code found at ${parameters.address}`, { + operation: "readContract", + chainId: options.chainId, + }); + } + } + return await retryRPC(() => client.readContract(parameters)) as T; + } catch (error) { + if (error instanceof RPCAdapterError) throw error; + throw requestError("readContract", options.chainId, error); + } + } + + return { + readContract, + getCode, + }; +} diff --git a/cli/src/adapters/rpc/errors.ts b/cli/src/adapters/rpc/errors.ts new file mode 100644 index 00000000..263aaea0 --- /dev/null +++ b/cli/src/adapters/rpc/errors.ts @@ -0,0 +1,55 @@ +import { statusCode } from "./utils"; + +/** Stable error categories emitted by the RPC adapter. */ +export type RPCErrorCode = + | "RPC_INVALID_CONFIGURATION" + | "RPC_ENV_VAR_MISSING" + | "RPC_CHAIN_NOT_FOUND" + | "RPC_CHAIN_ID_MISMATCH" + | "RPC_UNAUTHORIZED" + | "RPC_CONTRACT_NOT_FOUND" + | "RPC_REQUEST_FAILED" + | "RPC_INVALID_ADDRESS"; + +/** + * Diagnostic error thrown at the RPC/configuration boundary. + * + * The stable code is intended for programmatic handling; operation and chain + * ID provide context, while the original failure remains available as cause. + * @param code Stable adapter error category. + * @param message Human-readable diagnostic message. + * @param options Operation context and optional chain ID or original cause. + */ +export class RPCAdapterError extends Error { + /** Stable category used by callers and CLI error handling. */ + readonly code: RPCErrorCode; + /** RPC or configuration operation that produced the error. */ + readonly operation: string; + /** Chain involved in the operation, when one was resolved. */ + readonly chainId?: number; + + constructor( + code: RPCErrorCode, + message: string, + options: { operation: string; chainId?: number; cause?: unknown }, + ) { + super(message, { cause: options.cause }); + this.name = "RPCAdapterError"; + this.code = code; + this.operation = options.operation; + this.chainId = options.chainId; + } +} + +/** + * Converts an unknown transport failure into a stable adapter error. + * @param operation RPC operation that failed. + * @param chainId Chain on which the operation was attempted. + * @param error Original transport or client failure. + * @returns A normalized adapter error with a stable error code. + */ +export function requestError(operation: string, chainId: number, error: unknown): RPCAdapterError { + const status = statusCode(error); + const code = status === 401 ? "RPC_UNAUTHORIZED" : "RPC_REQUEST_FAILED"; + return new RPCAdapterError(code, `RPC ${operation} failed on chain ${chainId}`, { operation, chainId, cause: error }); +} diff --git a/cli/src/adapters/rpc/retry.ts b/cli/src/adapters/rpc/retry.ts new file mode 100644 index 00000000..52b29b56 --- /dev/null +++ b/cli/src/adapters/rpc/retry.ts @@ -0,0 +1,82 @@ +import { withRetry } from "viem"; +import { errorChain, errorText, statusCode } from "./utils"; + +/** Maximum number of retries after the initial RPC request. */ +const RETRY_COUNT = 2; +/** Fallback delays, in milliseconds, for successive retries. */ +const RETRY_DELAYS = [100, 200] as const; +/** Maximum delay accepted from a server Retry-After header. */ +const RETRY_AFTER_CAP = 2_000; + +/** + * Reads a Retry-After header and converts it to a bounded delay. + * @param error Error that may contain response headers. + * @returns Retry delay in milliseconds, or `undefined` when absent or invalid. + */ +function retryAfterMs(error: unknown): number | undefined { + for (const item of errorChain(error)) { + if (!item || typeof item !== "object") continue; + const headers = (item as { headers?: Headers; response?: { headers?: Headers } }).headers + ?? (item as { response?: { headers?: Headers } }).response?.headers; + const value = headers?.get("retry-after"); + if (!value) continue; + + const seconds = Number(value); + if (Number.isFinite(seconds)) return Math.min(Math.max(seconds * 1_000, 0), RETRY_AFTER_CAP); + + const timestamp = Date.parse(value); + if (!Number.isNaN(timestamp)) return Math.min(Math.max(timestamp - Date.now(), 0), RETRY_AFTER_CAP); + } + return undefined; +} + +/** + * Identifies failures that are safe to retry without changing chain state. + * @param error Error produced by the failed RPC request. + * @returns `true` when the failure is considered transient. + */ +function isTransientError(error: unknown): boolean { + const status = statusCode(error); + if (status === 401) return false; + if (status === 429 || status === 502 || status === 503) return true; + + for (const item of errorChain(error)) { + if (!item || typeof item !== "object") continue; + const code = (item as { code?: unknown }).code; + if (code === 429 || code === "ECONNRESET" || code === "ETIMEDOUT") return true; + } + + const text = errorText(error); + return [ + "econnreset", + "connection reset", + "fetch failed", + "network error", + "etimedout", + "econnrefused", + "socket hang up", + ].some((term) => text.includes(term)); +} + +/** + * Selects the server-provided delay or the adapter's backoff schedule. + * @param input Retry attempt count and failure information. + * @returns Delay before the next attempt, in milliseconds. + */ +function retryDelay({ count, error }: { count: number; error: Error }): number { + return retryAfterMs(error) ?? RETRY_DELAYS[Math.min(count, RETRY_DELAYS.length - 1)] ?? RETRY_DELAYS.at(-1)!; +} + +/** + * Executes an RPC request with bounded retries for transient failures only. + * @param request Asynchronous RPC operation to execute. + * @returns The successful request result. + * @throws The final request error when retries are exhausted or the failure is permanent. + */ +export async function retryRPC(request: () => Promise): Promise { + return withRetry(request, { + retryCount: RETRY_COUNT, + delay: retryDelay, + shouldRetry: ({ error }) => isTransientError(error), + }); +} diff --git a/cli/src/adapters/rpc/types.ts b/cli/src/adapters/rpc/types.ts new file mode 100644 index 00000000..c1ac1e16 --- /dev/null +++ b/cli/src/adapters/rpc/types.ts @@ -0,0 +1,7 @@ +/** Configuration used to bind an adapter to one RPC endpoint and chain. */ +export type RPCAdapterOptions = { + /** HTTP or HTTPS JSON-RPC endpoint. */ + rpcUrl: string; + /** Expected EVM chain ID reported by the endpoint. */ + chainId: number; +}; diff --git a/cli/src/adapters/rpc/utils.ts b/cli/src/adapters/rpc/utils.ts new file mode 100644 index 00000000..7ff008be --- /dev/null +++ b/cli/src/adapters/rpc/utils.ts @@ -0,0 +1,41 @@ +/** + * Returns an error and each distinct cause in its cause chain. + * @param error Error whose causes should be traversed. + * @returns The error and its reachable cause values in traversal order. + */ +export function errorChain(error: unknown): unknown[] { + const errors: unknown[] = []; + let current = error; + while (current && typeof current === "object" && !errors.includes(current)) { + errors.push(current); + current = "cause" in current ? (current as { cause?: unknown }).cause : undefined; + } + return errors; +} + +/** + * Combines error messages across an error's cause chain for matching. + * @param error Error whose messages should be combined. + * @returns Lowercase text containing all chained error messages. + */ +export function errorText(error: unknown): string { + return errorChain(error) + .map((item) => (item instanceof Error ? item.message : String(item))) + .join(" ") + .toLowerCase(); +} + +/** + * Finds an HTTP/status code in an error or one of its causes. + * @param error Error that may contain a status code. + * @returns The first numeric status code found, or `undefined`. + */ +export function statusCode(error: unknown): number | undefined { + for (const item of errorChain(error)) { + if (!item || typeof item !== "object") continue; + const value = item as { status?: unknown; statusCode?: unknown; response?: { status?: unknown } }; + const status = value.status ?? value.statusCode ?? value.response?.status; + if (typeof status === "number") return status; + } + return undefined; +} diff --git a/cli/src/adapters/rpc/validation.ts b/cli/src/adapters/rpc/validation.ts new file mode 100644 index 00000000..fb6a1100 --- /dev/null +++ b/cli/src/adapters/rpc/validation.ts @@ -0,0 +1,21 @@ +import { RPCAdapterError } from "./errors"; +import type { RPCAdapterOptions } from "./types"; + +/** + * Validates an RPC endpoint and expected chain before client construction. + * @param options RPC endpoint and expected chain configuration. + * @returns Nothing when the configuration is valid. + * @throws {RPCAdapterError} If the URL or chain ID is invalid. + */ +export function validateOptions({ rpcUrl, chainId }: RPCAdapterOptions): void { + if (!rpcUrl) throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "RPC URL is required", { operation: "create" }); + try { + const url = new URL(rpcUrl); + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("RPC URL must use HTTP or HTTPS"); + } catch (error) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "RPC URL must be a valid HTTP or HTTPS URL", { operation: "create", cause: error }); + } + if (!Number.isSafeInteger(chainId) || chainId <= 0) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "chainId must be a positive integer", { operation: "create", cause: chainId }); + } +} diff --git a/cli/src/comander.ts b/cli/src/comander.ts index c536b45b..6ec2043b 100644 --- a/cli/src/comander.ts +++ b/cli/src/comander.ts @@ -48,6 +48,18 @@ export function buildProgram(): Command { .command("build") .description("Compile the project if artifacts are stale or missing") + program + .command("rpc") + .description("Check RPC connectivity and optionally inspect contract bytecode") + .option("--chain ", "Chain key from compose.json", "local") + .option("--address
", "Contract address to check or inspect") + + program + .command("inspect") + .description("Inspect a deployed diamond's facets and selectors via Loupe") + .argument("
", "Diamond contract address") + .option("--chain ", "Chain key from compose.json", "local") + return program; } @@ -75,8 +87,12 @@ export function parseArgs(argv: string[]): { command: string; flags: Record typeof arg === "string" && arg !== command, ); - if (positionalArgs.length > 0 && !flags.projectName) { - flags.projectName = positionalArgs[0]; + if (positionalArgs.length > 0) { + if (command === "inspect" && !flags.address) { + flags.address = positionalArgs[0]; + } else if (!flags.projectName) { + flags.projectName = positionalArgs[0]; + } } return { command, flags }; diff --git a/cli/src/index.ts b/cli/src/index.ts index bb4a80f7..0795da69 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,13 +1,30 @@ +import dotenv from "dotenv"; +import path from "node:path"; import { Context } from "./context/context"; import { ComposeContext } from "./context/types"; import { EntryPipeline } from "./pipelines/entryPipeline"; import { exitWithError } from "./utils/errors"; import { parseArgs } from "./comander"; +function loadEnvFiles(): void { + const root = path.parse(process.cwd()).root; + let dir = process.cwd(); + const files: string[] = []; + while (dir !== root) { + files.push(path.join(dir, ".env")); + dir = path.dirname(dir); + } + files.push(path.join(root, ".env")); + for (const file of files.reverse()) { + dotenv.config({ path: file }); + } +} + /** * Main Entrypoint for the Compose CLI */ async function main(): Promise { + loadEnvFiles(); const { command, flags } = parseArgs(process.argv); const ctx: ComposeContext = Context.create(); diff --git a/cli/src/modules/inspect/abiLoader.ts b/cli/src/modules/inspect/abiLoader.ts new file mode 100644 index 00000000..b93b05a4 --- /dev/null +++ b/cli/src/modules/inspect/abiLoader.ts @@ -0,0 +1,102 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { keccak256, slice, toBytes } from "viem"; +import { FrameworkModule, type Framework } from "../../modules/framework/module"; +import type { AbiEntry } from "./types"; + +/** + * Extracts human-readable function signatures from a parsed ABI array. + * + * Only `function` entries with a `name` are included; fallback and receive + * entries are skipped. + * + * @param abi - The parsed ABI entries. + * @returns An array of signatures like `"transfer(address,uint256)"`. + */ +function extractSignaturesFromAbi(abi: AbiEntry[]): string[] { + const signatures: string[] = []; + for (const entry of abi) { + if (entry.type !== "function" || !entry.name) continue; + const params = entry.inputs?.map((i) => i.type).join(",") ?? ""; + signatures.push(`${entry.name}(${params})`); + } + return signatures; +} + +/** + * Computes the 4-byte selector for a function signature using Keccak-256. + * + * @param signature - The function signature (e.g., `"transfer(address,uint256)"`). + * @returns The lowercase hex-encoded 4-byte selector. + */ +function computeSelector(signature: string): string { + const hash = keccak256(toBytes(signature)); + return slice(hash, 0, 4).toLowerCase(); +} + +/** + * Reads all compiled ABI JSON files from the given directory. + * + * Expects Hardhat-style (`artifacts/`) or Foundry-style (`out/`) artifact + * layouts where each JSON file contains an `abi` array. + * + * @param dir - Absolute path to the artifacts directory. + * @returns An array of parsed ABI arrays, one per artifact file. + */ +async function readAbiFiles(dir: string): Promise { + const abis: AbiEntry[][] = []; + let entries: string[]; + try { + entries = await fs.readdir(dir, { recursive: true }); + } catch { + return abis; + } + for (const entry of entries) { + if (!entry.endsWith(".json")) continue; + const fullPath = path.join(dir, entry); + try { + const content = await fs.readFile(fullPath, "utf8"); + const parsed = JSON.parse(content); + if (Array.isArray(parsed?.abi)) { + abis.push(parsed.abi); + } + } catch { + // skip unparseable files + } + } + return abis; +} + +/** + * Loads all function signatures from the project's compiled ABI files and + * returns a selector-to-signature map. + * + * Automatically detects the framework (Hardhat or Foundry) to locate the + * correct artifact directory. + * + * @param projectRoot - Absolute path to the project root. + * @param framework - Optional framework override. When `null` or omitted the + * framework is auto-detected. + * @returns A map from lowercase 4-byte selectors to function signatures. + */ +export async function loadProjectSignatures( + projectRoot: string, + framework?: Framework | null, +): Promise> { + const fw = framework ?? FrameworkModule.detect(projectRoot); + const artifactDir = fw === "hardhat" + ? path.join(projectRoot, "artifacts") + : path.join(projectRoot, "out"); + + const abis = await readAbiFiles(artifactDir); + const map = new Map(); + for (const abi of abis) { + for (const sig of extractSignaturesFromAbi(abi)) { + const selector = computeSelector(sig); + if (!map.has(selector)) { + map.set(selector, sig); + } + } + } + return map; +} diff --git a/cli/src/modules/inspect/commonSignatures.ts b/cli/src/modules/inspect/commonSignatures.ts new file mode 100644 index 00000000..ba946b16 --- /dev/null +++ b/cli/src/modules/inspect/commonSignatures.ts @@ -0,0 +1,86 @@ +/** + * Standard 4-byte function signatures for common ERC and Diamond interfaces. + * + * Covers ERC-20, ERC-721, ERC-1155, Diamond Loupe/Cut, and Compose-specific + * functions. Used to build the initial selector-to-signature lookup map. + */ +export const COMMON_SIGNATURES: readonly string[] = [ + "acceptOwnership()", + "allowance(address,address)", + "allowance(address,address,uint256)", + "approve(address,uint256)", + "approve(address,uint256,uint256)", + "balanceOf(address)", + "balanceOf(address,uint256)", + "balanceOfBatch(address[],uint256[])", + "burn(address,uint256)", + "burn(address,uint256,uint256)", + "burn(uint256)", + "burn(uint256,uint256)", + "burnBatch(address,uint256[],uint256[])", + "burnBatch(uint256[])", + "burnFrom(address,uint256)", + "burnFrom(address,uint256,uint256)", + "checkTokenBridge(address)", + "crosschainBurn(address,uint256)", + "crosschainMint(address,uint256)", + "decimals()", + "deleteDefaultRoyalty()", + "diamondCut((address,bytes4[],uint8)[],address,bytes)", + "DOMAIN_SEPARATOR()", + "exportSelectors()", + "facetAddress(bytes4)", + "facetAddresses()", + "facetFunctionSelectors(address)", + "facets()", + "getApproved(uint256)", + "getRoleAdmin(bytes32)", + "getRoleExpiry(bytes32,address)", + "grantRole(bytes32,address)", + "grantRoleBatch(bytes32,address[])", + "grantRoleWithExpiry(bytes32,address,uint256)", + "hasRole(bytes32,address)", + "isApprovedForAll(address,address)", + "isOperator(address,address)", + "isRoleExpired(bytes32,address)", + "isRolePaused(bytes32)", + "name()", + "nonces(address)", + "owner()", + "ownerOf(uint256)", + "pauseRole(bytes32)", + "pendingOwner()", + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + "renounceOwnership()", + "renounceRole(bytes32,address)", + "requireRole(bytes32,address)", + "requireRoleNotPaused(bytes32,address)", + "requireValidRole(bytes32,address)", + "resetTokenRoyalty(uint256)", + "revokeRole(bytes32,address)", + "revokeRoleBatch(bytes32,address[])", + "royaltyInfo(uint256,uint256)", + "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", + "safeTransferFrom(address,address,uint256)", + "safeTransferFrom(address,address,uint256,bytes)", + "safeTransferFrom(address,address,uint256,uint256,bytes)", + "setApprovalForAll(address,bool)", + "setDefaultRoyalty(address,uint96)", + "setOperator(address,bool)", + "setRoleAdmin(bytes32,bytes32)", + "setTokenRoyalty(uint256,address,uint96)", + "supportsInterface(bytes4)", + "symbol()", + "tokenByIndex(uint256)", + "tokenOfOwnerByIndex(address,uint256)", + "tokenURI(uint256)", + "totalSupply()", + "transfer(address,uint256)", + "transfer(address,uint256,uint256)", + "transferFrom(address,address,uint256)", + "transferFrom(address,address,uint256,uint256)", + "transferOwnership(address)", + "unpauseRole(bytes32)", + "upgradeDiamond(address[],(address,address)[],address[],address,bytes,bytes32)", + "uri(uint256)", +]; diff --git a/cli/src/modules/inspect/diamondLoupeAbi.ts b/cli/src/modules/inspect/diamondLoupeAbi.ts new file mode 100644 index 00000000..b8f15b3c --- /dev/null +++ b/cli/src/modules/inspect/diamondLoupeAbi.ts @@ -0,0 +1,22 @@ +/** + * ABI for the EIP-2535 Diamond Loupe `facets()` view function. + * + * Used to query on-chain diamonds for their registered facets and selectors. + */ +export const DIAMOND_LOUPE_ABI = [ + { + name: "facets", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [ + { + type: "tuple[]", + components: [ + { name: "facet", type: "address" }, + { name: "functionSelectors", type: "bytes4[]" }, + ], + }, + ], + }, +] as const; diff --git a/cli/src/modules/inspect/facetFormatter.ts b/cli/src/modules/inspect/facetFormatter.ts new file mode 100644 index 00000000..490cfecc --- /dev/null +++ b/cli/src/modules/inspect/facetFormatter.ts @@ -0,0 +1,24 @@ +import { type Address, type Hex } from "viem"; +import { decodeSelector } from "./selectorDecoder"; +import type { FacetInfo } from "./types"; + +/** + * Converts raw on-chain facet data into a {@link FacetInfo} object with decoded + * selector signatures. + * + * @param raw - The raw facet returned by the Diamond Loupe. + * @param raw.facet - The facet contract address. + * @param raw.functionSelectors - The 4-byte selectors registered on the facet. + * @param index - The zero-based index of the facet in the Loupe response. + * @returns The facet info with decoded selectors. + */ +export function toFacetInfo(raw: { facet: Address; functionSelectors: Hex[] }, index: number): FacetInfo { + return { + address: raw.facet, + index, + selectors: raw.functionSelectors.map((sel) => ({ + selector: sel, + signature: decodeSelector(sel), + })), + }; +} diff --git a/cli/src/modules/inspect/module.ts b/cli/src/modules/inspect/module.ts new file mode 100644 index 00000000..6798c9cb --- /dev/null +++ b/cli/src/modules/inspect/module.ts @@ -0,0 +1,86 @@ +import path from "node:path"; +import { isAddress, type Address, type Hex } from "viem"; +import { ComposeContext } from "../../context/types"; +import { DependencyKey } from "../../resolver/dependencyKey"; +import { DependencyResolver } from "../../resolver/dependencyResolver"; +import { resolveChainConfig } from "../../utils/chainConfig"; +import { findFileAncestor } from "../../utils/files"; +import { RPCAdapterError } from "../../adapters/rpc/errors"; +import { showInspect } from "./output"; +import { DIAMOND_LOUPE_ABI } from "./diamondLoupeAbi"; +import { toFacetInfo } from "./facetFormatter"; +import { mergeProjectSignatures } from "./selectorDecoder"; +import type { InspectResult, FacetInfo } from "./types"; + +export const InspectModule = { + /** + * Inspects an on-chain Diamond and displays its facets and selectors. + * + * Validates the diamond address, resolves the RPC adapter for the target + * chain, fetches facets via the Diamond Loupe, and decodes each selector + * using a combination of common signatures and project ABI files. + * + * @param ctx - The compose context with `address` and optional `chain` params. + * @returns The updated context with inspect result stored in + * `ctx.state.inspect` as {@link ModuleState}\<{@link InspectResult}\>. + * @throws {RPCAdapterError} If the address is invalid or no contract code is + * found. + */ + async inspect(ctx: ComposeContext): Promise { + const addressValue = ctx.param.address; + if (typeof addressValue !== "string" || !isAddress(addressValue, { strict: false })) { + throw new RPCAdapterError( + "RPC_INVALID_ADDRESS", + `Invalid diamond address: ${String(addressValue ?? "")}`, + { operation: "inspect" }, + ); + } + const diamondAddress = addressValue as Address; + + const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local"; + const configuredChain = await resolveChainConfig({ chainKey }); + + const dependencies = await DependencyResolver.resolve([{ + key: DependencyKey.RPC, + params: { chainKey: configuredChain.chainKey }, + }]); + const rpc = dependencies[DependencyKey.RPC]; + if (!rpc) throw new Error("RPC dependency was not resolved"); + + const code = await rpc.getCode(diamondAddress); + if (!code || code === "0x") { + throw new RPCAdapterError( + "RPC_CONTRACT_NOT_FOUND", + `No contract code found at ${diamondAddress}`, + { operation: "inspect", chainId: configuredChain.chainId }, + ); + } + + const rawFacets = await rpc.readContract< + { facet: Address; functionSelectors: Hex[] }[] + >({ + address: diamondAddress, + abi: DIAMOND_LOUPE_ABI, + functionName: "facets", + }); + + const composePath = await findFileAncestor(process.cwd(), "compose.json"); + const projectRoot = composePath ? path.dirname(composePath) : null; + if (projectRoot) { + await mergeProjectSignatures(projectRoot); + } + + const facets: FacetInfo[] = rawFacets.map(toFacetInfo); + + const result: InspectResult = { + diamond: diamondAddress, + chainKey: configuredChain.chainKey, + chainId: configuredChain.chainId, + facets, + }; + + ctx.state.inspect = { success: true, result, error: null }; + showInspect(result); + return ctx; + }, +}; diff --git a/cli/src/modules/inspect/output.ts b/cli/src/modules/inspect/output.ts new file mode 100644 index 00000000..95b0ed52 --- /dev/null +++ b/cli/src/modules/inspect/output.ts @@ -0,0 +1,47 @@ +import { cyan, dim, green } from "../../utils/terminal"; +import type { InspectResult } from "./types"; + +const TREE_BRANCH = "├── "; +const TREE_LAST = "└── "; +const TREE_PIPE = "│ "; +const TREE_SPACE = " "; + +/** + * Prints a formatted summary of the inspect result to the console. + * + * Displays the diamond address, chain, facet count, total selector count, and + * each facet with its decoded selectors in a tree layout. + * + * @param result - The inspect result containing the diamond info and facets. + */ +export function showInspect(result: InspectResult): void { + const totalSelectors = result.facets.reduce((sum, f) => sum + f.selectors.length, 0); + + console.log(`\n${cyan("Diamond Inspect")}\n`); + console.log(` ${result.diamond}`); + console.log(` ${dim(`${result.chainKey} (${result.chainId}) · ${result.facets.length} facets · ${totalSelectors} selectors`)}`); + console.log(); + + for (let i = 0; i < result.facets.length; i++) { + const facet = result.facets[i]; + const isLast = i === result.facets.length - 1; + const branch = isLast ? TREE_LAST : TREE_BRANCH; + const childPrefix = isLast ? TREE_SPACE : TREE_PIPE; + + console.log(` ${dim(branch)}${facet.address}`); + + for (let j = 0; j < facet.selectors.length; j++) { + const sel = facet.selectors[j]; + const isLastSelector = j === facet.selectors.length - 1; + const selectorBranch = isLastSelector ? TREE_LAST : TREE_BRANCH; + + console.log(` ${dim(childPrefix + selectorBranch)}${dim(sel.selector)} ${green(sel.signature)}`); + } + + if (!isLast) { + console.log(` ${dim(TREE_PIPE)}`); + } + } + + console.log(); +} diff --git a/cli/src/modules/inspect/selectorDecoder.ts b/cli/src/modules/inspect/selectorDecoder.ts new file mode 100644 index 00000000..c5f5a56b --- /dev/null +++ b/cli/src/modules/inspect/selectorDecoder.ts @@ -0,0 +1,54 @@ +import { keccak256, slice, toBytes, type Hex } from "viem"; +import { loadProjectSignatures } from "./abiLoader"; +import { COMMON_SIGNATURES } from "./commonSignatures"; + +const SELECTOR_MAP = new Map(); + +/** + * Lazily populates the selector lookup map from {@link COMMON_SIGNATURES}. + * Subsequent calls are no-ops once the map is initialized. + */ +function ensureSelectorMap(): void { + if (SELECTOR_MAP.size > 0) return; + for (const sig of COMMON_SIGNATURES) { + const trimmed = sig.trim(); + if (!trimmed) continue; + const hash = keccak256(toBytes(trimmed)); + const selector = slice(hash, 0, 4); + SELECTOR_MAP.set(selector.toLowerCase(), trimmed); + } +} + +/** + * Decodes a 4-byte function selector into its human-readable signature. + * + * Falls back to returning the raw hex when no match is found in either the + * common signatures or the project-specific ABI files. + * + * @param selector - The 4-byte selector (e.g., `"0x313ce567"`). + * @returns The matched function signature, or the original hex string. + */ +export function decodeSelector(selector: string | Hex): string { + ensureSelectorMap(); + const key = selector.toLowerCase(); + return SELECTOR_MAP.get(key) ?? selector; +} + +/** + * Augments the selector lookup map with signatures extracted from the project's + * compiled ABI files (artifacts or out directory). + * + * Existing entries are never overwritten, so common signatures take precedence. + * + * @param projectRoot - Absolute path to the project root containing + * `compose.json`. + */ +export async function mergeProjectSignatures(projectRoot: string): Promise { + ensureSelectorMap(); + const projectMap = await loadProjectSignatures(projectRoot); + for (const [selector, signature] of projectMap) { + if (!SELECTOR_MAP.has(selector)) { + SELECTOR_MAP.set(selector, signature); + } + } +} diff --git a/cli/src/modules/inspect/types.ts b/cli/src/modules/inspect/types.ts new file mode 100644 index 00000000..796a4c6e --- /dev/null +++ b/cli/src/modules/inspect/types.ts @@ -0,0 +1,29 @@ +import type { Address } from "viem"; + +/** A single entry parsed from a contract ABI JSON array. */ +export type AbiEntry = { + type: string; + name?: string; + inputs?: { type: string }[]; +}; + +/** A 4-byte selector paired with its decoded function signature. */ +export type SelectorInfo = { + selector: string; + signature: string; +}; + +/** Information about a single facet registered on the Diamond. */ +export type FacetInfo = { + address: Address; + index: number; + selectors: SelectorInfo[]; +}; + +/** The complete result of inspecting an on-chain Diamond. */ +export type InspectResult = { + diamond: Address; + chainKey: string; + chainId: number; + facets: FacetInfo[]; +}; diff --git a/cli/src/modules/pipelineBuilder/module.ts b/cli/src/modules/pipelineBuilder/module.ts index 606282d9..1e16d459 100644 --- a/cli/src/modules/pipelineBuilder/module.ts +++ b/cli/src/modules/pipelineBuilder/module.ts @@ -3,6 +3,8 @@ import { InitPipeline } from "../../pipelines/initPipeline"; import { InfoPipeline } from "../../pipelines/infoPipeline"; import { CatalogPipeline } from "../../pipelines/catalogPipeline"; import { BuildPipeline } from "../../pipelines/buildPipeline"; +import { RPCPipeline } from "../../pipelines/rpcPipeline"; +import { InspectPipeline } from "../../pipelines/inspectPipeline"; /** * Pipeline builder module that routes CLI commands to their corresponding pipelines. @@ -59,6 +61,20 @@ export const PipelineBuilderModule = { error: null, }; return BuildPipeline.execute(ctx); + case "rpc": + ctx.state.commandSelected = { + success: true, + result: { command: ctx.param.command, chain: ctx.param.chain }, + error: null, + }; + return RPCPipeline.execute(ctx); + case "inspect": + ctx.state.commandSelected = { + success: true, + result: { command: ctx.param.command, address: ctx.param.address, chain: ctx.param.chain }, + error: null, + }; + return InspectPipeline.execute(ctx); default: ctx.state.commandRouting = { success: false, diff --git a/cli/src/modules/rpc/module.ts b/cli/src/modules/rpc/module.ts new file mode 100644 index 00000000..5ebcbe4e --- /dev/null +++ b/cli/src/modules/rpc/module.ts @@ -0,0 +1,47 @@ +import { isAddress, type Address } from "viem"; +import { ComposeContext } from "../../context/types"; +import { DependencyKey } from "../../resolver/dependencyKey"; +import { DependencyResolver } from "../../resolver/dependencyResolver"; +import { resolveChainConfig } from "../../utils/chainConfig"; +import { showRPCCheck } from "./output"; +import type { RPCCheckResult } from "./types"; + +/** Runs the CLI's real-RPC smoke test without domain-specific contract logic. */ +export const RPCModule = { + async check(ctx: ComposeContext): Promise { + const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local"; + const configuredChain = await resolveChainConfig({ chainKey }); + const addressValue = ctx.param.address; + + let address: Address | undefined; + if (addressValue !== undefined) { + if (typeof addressValue !== "string" || !isAddress(addressValue, { strict: false })) { + throw new Error(`Invalid contract address: ${String(addressValue)}`); + } + address = addressValue as Address; + } + + const dependencies = await DependencyResolver.resolve([{ + key: DependencyKey.RPC, + params: { chainKey: configuredChain.chainKey }, + }]); + const rpc = dependencies[DependencyKey.RPC]; + if (!rpc) throw new Error("RPC dependency was not resolved"); + + let hasCode: boolean | undefined; + if (address) { + const code = await rpc.getCode(address); + hasCode = Boolean(code && code !== "0x"); + } + + const result: RPCCheckResult = { + chainKey: configuredChain.chainKey, + chainId: configuredChain.chainId, + ...(address ? { address, hasCode } : {}), + }; + + ctx.state.rpcCheck = { success: true, result, error: null }; + showRPCCheck(result); + return ctx; + }, +}; diff --git a/cli/src/modules/rpc/output.ts b/cli/src/modules/rpc/output.ts new file mode 100644 index 00000000..50551649 --- /dev/null +++ b/cli/src/modules/rpc/output.ts @@ -0,0 +1,18 @@ +import { cyan, dim, green, yellow } from "../../utils/terminal"; +import type { RPCCheckResult } from "./types"; + +/** Displays the result of a real RPC connectivity check. */ +export function showRPCCheck(result: RPCCheckResult): void { + console.log(`\n${cyan("RPC Check")}\n`); + console.log(` ${dim("Chain:")} ${result.chainKey}`); + console.log(` ${dim("Chain ID:")} ${result.chainId}`); + console.log(` ${dim("Endpoint:")} ${green("connected")}`); + + if (result.address) { + const status = result.hasCode ? green("deployed") : yellow("no bytecode"); + console.log(` ${dim("Address:")} ${result.address}`); + console.log(` ${dim("Bytecode:")} ${status}`); + } + + console.log(` ${dim("Result:")} ${green("passed")}\n`); +} diff --git a/cli/src/modules/rpc/types.ts b/cli/src/modules/rpc/types.ts new file mode 100644 index 00000000..a1339ef9 --- /dev/null +++ b/cli/src/modules/rpc/types.ts @@ -0,0 +1,8 @@ +import type { Address } from "viem"; + +export type RPCCheckResult = { + chainKey: string; + chainId: number; + address?: Address; + hasCode?: boolean; +}; diff --git a/cli/src/pipelines/inspectPipeline.ts b/cli/src/pipelines/inspectPipeline.ts new file mode 100644 index 00000000..88ca5a80 --- /dev/null +++ b/cli/src/pipelines/inspectPipeline.ts @@ -0,0 +1,9 @@ +import { ComposeContext } from "../context/types"; +import { InspectModule } from "../modules/inspect/module"; + +/** Diamond inspect pipeline for querying on-chain facets via Loupe. */ +export const InspectPipeline = { + async execute(ctx: ComposeContext): Promise { + return InspectModule.inspect(ctx); + }, +}; diff --git a/cli/src/pipelines/rpcPipeline.ts b/cli/src/pipelines/rpcPipeline.ts new file mode 100644 index 00000000..1b992829 --- /dev/null +++ b/cli/src/pipelines/rpcPipeline.ts @@ -0,0 +1,9 @@ +import { ComposeContext } from "../context/types"; +import { RPCModule } from "../modules/rpc/module"; + +/** RPC smoke-test pipeline for validating a configured chain endpoint. */ +export const RPCPipeline = { + async execute(ctx: ComposeContext): Promise { + return RPCModule.check(ctx); + }, +}; diff --git a/cli/src/resolver/dependencyKey.ts b/cli/src/resolver/dependencyKey.ts index 804380ad..37e77ec0 100644 --- a/cli/src/resolver/dependencyKey.ts +++ b/cli/src/resolver/dependencyKey.ts @@ -6,6 +6,7 @@ */ export enum DependencyKey { Hashing = "hashing", + RPC = "rpc", Foundry = "foundry", Hardhat = "hardhat", } diff --git a/cli/src/resolver/dependencyRegistry.ts b/cli/src/resolver/dependencyRegistry.ts index 2fa85052..7c4b9ecd 100644 --- a/cli/src/resolver/dependencyRegistry.ts +++ b/cli/src/resolver/dependencyRegistry.ts @@ -5,11 +5,19 @@ import { IHashingAdapter } from "../adapters/interface/IHashingAdapter"; import { IFrameworkAdapter } from "../adapters/interface/IFrameworkAdapter"; import { foundryAdapter } from "../adapters/foundryAdapter"; import { hardhatAdapter } from "../adapters/hardhatAdapter"; +import type { IRPCAdapter } from "../adapters/interface/IRPCAdapter"; +import { createRPCAdapter } from "../adapters/rpc/adapter"; +import { resolveChainConfig } from "../utils/chainConfig"; import { DependencyKey } from "./dependencyKey"; /** Optional parameters passed to a dependency factory. */ export type DependencyParams = Record; +type RPCDependencyParams = DependencyParams & { + chainKey?: unknown; + projectRoot?: string; +}; + /** Factory function that creates or returns a dependency instance. */ export type DependencyFactory = ( params?: DependencyParams, @@ -18,6 +26,7 @@ export type DependencyFactory = ( /** Typed map of all dependency keys to their resolved adapter types. */ export type DependencyMap = { [DependencyKey.Hashing]: IHashingAdapter; + [DependencyKey.RPC]: IRPCAdapter; [DependencyKey.Foundry]: IFrameworkAdapter; [DependencyKey.Hardhat]: IFrameworkAdapter; }; @@ -36,6 +45,10 @@ export const DependencyRegistry: { [Key in DependencyKey]: DependencyFactory; } = { [DependencyKey.Hashing]: () => HashingAdapter, + [DependencyKey.RPC]: async (params) => { + const resolved = await resolveChainConfig(params as RPCDependencyParams | undefined); + return createRPCAdapter({ rpcUrl: resolved.rpcUrl, chainId: resolved.chainId }); + }, [DependencyKey.Foundry]: () => foundryAdapter, [DependencyKey.Hardhat]: () => hardhatAdapter, }; diff --git a/cli/src/resolver/dependencyResolver.ts b/cli/src/resolver/dependencyResolver.ts index 877060ab..2b5d64e1 100644 --- a/cli/src/resolver/dependencyResolver.ts +++ b/cli/src/resolver/dependencyResolver.ts @@ -41,7 +41,7 @@ export const DependencyResolver = { throw new Error(`Dependency factory not found: ${request.key}`); } - deps[request.key] = await factory(request.params) as any; + Object.assign(deps, { [request.key]: await factory(request.params) }); } return deps; diff --git a/cli/src/utils/chainConfig.ts b/cli/src/utils/chainConfig.ts new file mode 100644 index 00000000..f360686c --- /dev/null +++ b/cli/src/utils/chainConfig.ts @@ -0,0 +1,94 @@ +import fs from "node:fs/promises"; +import { findFileAncestor } from "./files"; +import { RPCAdapterError } from "../adapters/rpc/errors"; + +export type ResolvedChainConfig = { + /** Name used to select the chain in compose.json. */ + chainKey: string; + /** RPC URL after environment-variable interpolation. */ + rpcUrl: string; + /** Expected EVM chain ID for the selected endpoint. */ + chainId: number; +}; + +export type ResolveChainOptions = { + /** Chain key; defaults to `local`. */ + chainKey?: unknown; + /** Directory from which to search upward for compose.json. */ + projectRoot?: string; +}; + +const ENVIRONMENT_VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g; + +function interpolateRPCUrl(value: string): string { + const missing = new Set(); + const resolved = value.replace(ENVIRONMENT_VARIABLE, (_, name: string) => { + const environmentValue = process.env[name]; + if (!environmentValue) { + missing.add(name); + return ""; + } + return environmentValue; + }); + + if (missing.size > 0) { + throw new RPCAdapterError( + "RPC_ENV_VAR_MISSING", + `Missing environment variable(s) for RPC URL: ${[...missing].join(", ")}`, + { operation: "resolveChain" }, + ); + } + return resolved; +} + +/** + * Loads and validates a chain entry from the nearest compose.json. + * + * RPC URLs may contain `${NAME}` placeholders, which are resolved from the + * process environment before the configuration is returned. + */ +export async function resolveChainConfig(options: ResolveChainOptions = {}): Promise { + if (options.chainKey !== undefined && typeof options.chainKey !== "string") { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "chainKey must be a string", { operation: "resolveChain" }); + } + + const chainKey = options.chainKey === undefined ? "local" : options.chainKey; + const startDir = options.projectRoot ?? process.cwd(); + const composePath = await findFileAncestor(startDir, "compose.json"); + if (!composePath) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "compose.json not found", { operation: "resolveChain" }); + } + + let composeJson: unknown; + try { + composeJson = JSON.parse(await fs.readFile(composePath, "utf8")); + } catch (error) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", `Unable to parse ${composePath}`, { operation: "resolveChain", cause: error }); + } + + const root = composeJson && typeof composeJson === "object" ? composeJson as Record : undefined; + const chains = root?.chains; + if (!chains || typeof chains !== "object") { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", "compose.json is missing a chains object", { operation: "resolveChain" }); + } + + const chain = (chains as Record)[chainKey]; + if (!chain || typeof chain !== "object") { + throw new RPCAdapterError("RPC_CHAIN_NOT_FOUND", `Chain '${chainKey}' was not found in compose.json`, { operation: "resolveChain" }); + } + + const entry = chain as Record; + if (typeof entry.rpc !== "string" || entry.rpc.trim() === "") { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", `Chain '${chainKey}' has an invalid rpc value`, { operation: "resolveChain" }); + } + if (!Number.isSafeInteger(entry.chainId) || (entry.chainId as number) <= 0) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", `Chain '${chainKey}' has an invalid chainId`, { operation: "resolveChain" }); + } + + const rpcUrl = interpolateRPCUrl(entry.rpc); + if (!rpcUrl.trim()) { + throw new RPCAdapterError("RPC_INVALID_CONFIGURATION", `Chain '${chainKey}' resolved to an empty RPC URL`, { operation: "resolveChain" }); + } + + return { chainKey, rpcUrl, chainId: entry.chainId as number }; +} diff --git a/cli/src/utils/files.ts b/cli/src/utils/files.ts index 75521092..c12d0dd7 100644 --- a/cli/src/utils/files.ts +++ b/cli/src/utils/files.ts @@ -116,11 +116,12 @@ export async function isDirectoryEmpty(dirPath: string, ignoredEntries: string[] } /** - * Searches for a file by walking up the directory tree from the given start path. + * Searches for a file by walking up the directory tree from the given start + * path. * * @param startDir - The directory to start searching from. - * @param fileName - The name of the file to find (e.g., "compose.json"). - * @returns The full path to the file, or null if not found. + * @param fileName - The name of the file to find (e.g., `"compose.json"`). + * @returns The full path to the file, or `null` if not found. */ export async function findFileAncestor(startDir: string, fileName: string): Promise { let currentDir = startDir; diff --git a/cli/test/adapters/rpcAdapter.test.ts b/cli/test/adapters/rpcAdapter.test.ts new file mode 100644 index 00000000..9921982c --- /dev/null +++ b/cli/test/adapters/rpcAdapter.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + activeClient: undefined as unknown, + delays: [] as number[], + createPublicClient: vi.fn(), + withRetry: vi.fn(async (request: () => Promise, options: { + retryCount: number; + delay: ({ count, error }: { count: number; error: Error }) => number; + shouldRetry: ({ error }: { error: Error }) => boolean; + }) => { + let count = 0; + while (true) { + try { + return await request(); + } catch (error) { + if (count >= options.retryCount || !options.shouldRetry({ error: error as Error })) throw error; + mocks.delays.push(options.delay({ count, error: error as Error })); + count += 1; + } + } + }), +})); + +vi.mock("viem", () => ({ + createPublicClient: mocks.createPublicClient.mockImplementation(() => mocks.activeClient), + defineChain: (chain: unknown) => chain, + getAddress: (address: string) => address, + http: (_url: string, _options: unknown) => ({ type: "http" }), + withRetry: mocks.withRetry, +})); + +import { createRPCAdapter } from "../../src/adapters/rpc/adapter"; + +type MockClient = { + chain: { id: number }; + getChainId: ReturnType; + getCode: ReturnType; + readContract: ReturnType; +}; + +const address = "0x0000000000000000000000000000000000000001" as `0x${string}`; + +function useClient(overrides: Record = {}): void { + mocks.delays.length = 0; + mocks.activeClient = { + chain: { id: 11155111 }, + getChainId: vi.fn().mockResolvedValue(11155111), + getCode: vi.fn().mockResolvedValue("0x6000"), + readContract: vi.fn().mockResolvedValue("result"), + ...overrides, + }; +} + +describe("createRPCAdapter", () => { + it("verifies the endpoint chain and delegates generic reads", async () => { + useClient(); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + const result = await adapter.readContract({ + address, + abi: [], + functionName: "example", + } as never); + + expect(result).toBe("result"); + expect((mocks.activeClient as MockClient).readContract).toHaveBeenCalledOnce(); + }); + + it("fails immediately when the endpoint chain does not match", async () => { + useClient({ getChainId: vi.fn().mockResolvedValue(1) }); + + await expect(createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 })).rejects.toMatchObject({ + code: "RPC_CHAIN_ID_MISMATCH", + }); + }); + + it("checks bytecode only when requested", async () => { + useClient(); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + await adapter.readContract({ address, abi: [], functionName: "example" } as never); + expect((mocks.activeClient as MockClient).getCode).not.toHaveBeenCalled(); + + await adapter.readContract({ address, abi: [], functionName: "example" } as never, { verifyCode: true }); + expect((mocks.activeClient as MockClient).getCode).toHaveBeenCalledOnce(); + }); + + it("reports missing contract code when verification is enabled", async () => { + useClient({ getCode: vi.fn().mockResolvedValue("0x") }); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + + await expect(adapter.readContract({ address, abi: [], functionName: "example" } as never, { verifyCode: true })).rejects.toMatchObject({ + code: "RPC_CONTRACT_NOT_FOUND", + }); + expect((mocks.activeClient as MockClient).readContract).not.toHaveBeenCalled(); + }); + + it("makes three total attempts for transient failures", async () => { + const request = vi.fn() + .mockRejectedValueOnce(Object.assign(new Error("temporary"), { status: 503 })) + .mockRejectedValueOnce(Object.assign(new Error("temporary"), { status: 503 })) + .mockResolvedValue("result"); + useClient({ readContract: request }); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + + await expect(adapter.readContract({ address, abi: [], functionName: "example" } as never)).resolves.toBe("result"); + expect(request).toHaveBeenCalledTimes(3); + expect(mocks.delays).toEqual([100, 200]); + }); + + it("retries JSON-RPC throttling errors and honors a capped Retry-After header", async () => { + const headers = { get: vi.fn().mockReturnValue("10") }; + const request = vi.fn() + .mockRejectedValueOnce(Object.assign(new Error("rate limited"), { code: 429, headers })) + .mockResolvedValue("result"); + useClient({ readContract: request }); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + + await expect(adapter.readContract({ address, abi: [], functionName: "example" } as never)).resolves.toBe("result"); + expect(request).toHaveBeenCalledTimes(2); + expect(mocks.delays).toEqual([2_000]); + }); + + it("does not retry unauthorized failures", async () => { + const request = vi.fn().mockRejectedValue(Object.assign(new Error("unauthorized"), { status: 401 })); + useClient({ readContract: request }); + const adapter = await createRPCAdapter({ rpcUrl: "https://rpc.example", chainId: 11155111 }); + + await expect(adapter.readContract({ address, abi: [], functionName: "example" } as never)).rejects.toMatchObject({ code: "RPC_UNAUTHORIZED" }); + expect(request).toHaveBeenCalledOnce(); + }); +}); diff --git a/cli/test/comander.test.ts b/cli/test/comander.test.ts new file mode 100644 index 00000000..65e5bf91 --- /dev/null +++ b/cli/test/comander.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { parseArgs } from "../src/comander"; + +describe("rpc command", () => { + it("parses chain and optional address flags", () => { + const result = parseArgs([ + "node", + "compose", + "rpc", + "--chain", + "sepolia", + "--address", + "0x0000000000000000000000000000000000000001", + ]); + + expect(result).toEqual({ + command: "rpc", + flags: { + chain: "sepolia", + address: "0x0000000000000000000000000000000000000001", + }, + }); + }); + + it("defaults rpc checks to local", () => { + expect(parseArgs(["node", "compose", "rpc"])).toEqual({ + command: "rpc", + flags: { chain: "local" }, + }); + }); +}); + +describe("inspect command", () => { + it("parses address positional and chain flag", () => { + const result = parseArgs([ + "node", + "compose", + "inspect", + "0x0000000000000000000000000000000000000001", + "--chain", + "sepolia", + ]); + + expect(result).toEqual({ + command: "inspect", + flags: { + address: "0x0000000000000000000000000000000000000001", + chain: "sepolia", + }, + }); + }); + + it("defaults chain to local", () => { + const result = parseArgs([ + "node", + "compose", + "inspect", + "0x0000000000000000000000000000000000000001", + ]); + + expect(result).toEqual({ + command: "inspect", + flags: { + address: "0x0000000000000000000000000000000000000001", + chain: "local", + }, + }); + }); + +}); diff --git a/cli/test/modules/inspect/abiLoader.test.ts b/cli/test/modules/inspect/abiLoader.test.ts new file mode 100644 index 00000000..c850364b --- /dev/null +++ b/cli/test/modules/inspect/abiLoader.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, afterEach } from "vitest"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { loadProjectSignatures } from "../../../src/modules/inspect/abiLoader"; + +let tmpDir: string; + +async function setup(projectStructure: Record): Promise { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "abiLoader-test-")); + await writeStructure(tmpDir, projectStructure); + return tmpDir; +} + +async function writeStructure(dir: string, structure: Record): Promise { + for (const [name, value] of Object.entries(structure)) { + const fullPath = path.join(dir, name); + if (typeof value === "string") { + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, value, "utf8"); + } else if (typeof value === "object" && value !== null) { + await fs.mkdir(fullPath, { recursive: true }); + await writeStructure(fullPath, value as Record); + } + } +} + +afterEach(async () => { + if (tmpDir) { + await fs.rm(tmpDir, { recursive: true, force: true }); + } +}); + +describe("loadProjectSignatures", () => { + it("extracts function signatures from Foundry artifacts", async () => { + await setup({ + out: { + "CounterFacet.sol": { + "CounterFacet.json": JSON.stringify({ + abi: [ + { type: "function", name: "getNumber", inputs: [], outputs: [{ type: "uint256" }] }, + { type: "function", name: "setNumber", inputs: [{ name: "num", type: "uint256" }], outputs: [] }, + ], + }), + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.get("0xf2c9ecd8")).toBe("getNumber()"); + expect(map.get("0x3fb5c1cb")).toBe("setNumber(uint256)"); + }); + + it("extracts function signatures from Hardhat artifacts", async () => { + await setup({ + artifacts: { + contracts: { + "CounterFacet.sol": { + "CounterFacet.json": JSON.stringify({ + abi: [ + { type: "function", name: "increment", inputs: [], outputs: [] }, + ], + }), + }, + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "hardhat"); + + expect(map.get("0xd09de08a")).toBe("increment()"); + }); + + it("handles multiple ABI files", async () => { + await setup({ + out: { + "FacetA.sol": { + "FacetA.json": JSON.stringify({ + abi: [{ type: "function", name: "foo", inputs: [], outputs: [] }], + }), + }, + "FacetB.sol": { + "FacetB.json": JSON.stringify({ + abi: [{ type: "function", name: "bar", inputs: [{ type: "uint256" }], outputs: [] }], + }), + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.size).toBeGreaterThanOrEqual(2); + expect(map.has("0xc2985578")).toBe(true); + expect(map.has("0x0423a132")).toBe(true); + }); + + it("skips non-function ABI entries", async () => { + await setup({ + out: { + "Facet.sol": { + "Facet.json": JSON.stringify({ + abi: [ + { type: "function", name: "foo", inputs: [], outputs: [] }, + { type: "event", name: "Transfer" }, + { type: "constructor", inputs: [] }, + { type: "error", name: "InsufficientBalance" }, + ], + }), + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.size).toBe(1); + }); + + it("returns empty map when artifact directory doesn't exist", async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "abiLoader-empty-")); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.size).toBe(0); + }); + + it("skips unparseable JSON files", async () => { + await setup({ + out: { + "Bad.sol": { + "Bad.json": "not valid json {{{", + }, + "Good.sol": { + "Good.json": JSON.stringify({ + abi: [{ type: "function", name: "ok", inputs: [], outputs: [] }], + }), + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.size).toBe(1); + }); + + it("skips JSON files without abi field", async () => { + await setup({ + out: { + "NoAbi.sol": { + "NoAbi.json": JSON.stringify({ bytecode: "0x6000" }), + }, + }, + }); + + const map = await loadProjectSignatures(tmpDir, "foundry"); + + expect(map.size).toBe(0); + }); +}); diff --git a/cli/test/modules/inspect/module.test.ts b/cli/test/modules/inspect/module.test.ts new file mode 100644 index 00000000..dfd7d3c4 --- /dev/null +++ b/cli/test/modules/inspect/module.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it, vi } from "vitest"; +import { Context } from "../../../src/context/context"; +import { DependencyKey } from "../../../src/resolver/dependencyKey"; +import { decodeSelector } from "../../../src/modules/inspect/selectorDecoder"; + +const mocks = vi.hoisted(() => ({ + resolveChainConfig: vi.fn(), + resolve: vi.fn(), + showInspect: vi.fn(), +})); + +vi.mock("../../../src/utils/chainConfig", () => ({ resolveChainConfig: mocks.resolveChainConfig })); +vi.mock("../../../src/resolver/dependencyResolver", () => ({ DependencyResolver: { resolve: mocks.resolve } })); +vi.mock("../../../src/modules/inspect/output", () => ({ showInspect: mocks.showInspect })); + +import { InspectModule } from "../../../src/modules/inspect/module"; + +const VALID_ADDRESS = "0x0000000000000000000000000000000000000001"; + +describe("InspectModule", () => { + it("inspects a diamond and displays facets", async () => { + const readContract = vi.fn().mockResolvedValue([ + { + facet: "0x0000000000000000000000000000000000000002", + functionSelectors: ["0x313ce567", "0x18160ddd"], + }, + { + facet: "0x0000000000000000000000000000000000000003", + functionSelectors: ["0x095ea7b3"], + }, + ]); + const getCode = vi.fn().mockResolvedValue("0x6000"); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + + const ctx = Context.create(); + ctx.param = { address: VALID_ADDRESS, chain: "sepolia" }; + + const result = await InspectModule.inspect(ctx); + + expect(mocks.resolve).toHaveBeenCalledWith([{ + key: DependencyKey.RPC, + params: { chainKey: "sepolia" }, + }]); + expect(readContract).toHaveBeenCalledWith({ + address: VALID_ADDRESS, + abi: expect.any(Array), + functionName: "facets", + }); + expect(result.state.inspect).toMatchObject({ + success: true, + result: { + diamond: VALID_ADDRESS, + chainKey: "sepolia", + chainId: 11155111, + facets: [ + { address: "0x0000000000000000000000000000000000000002", index: 0, selectors: expect.any(Array) }, + { address: "0x0000000000000000000000000000000000000003", index: 1, selectors: expect.any(Array) }, + ], + }, + }); + expect(mocks.showInspect).toHaveBeenCalledOnce(); + }); + + it("throws RPC_INVALID_ADDRESS for an invalid address", async () => { + const ctx = Context.create(); + ctx.param = { address: "not-an-address", chain: "sepolia" }; + + await expect(InspectModule.inspect(ctx)).rejects.toMatchObject({ + code: "RPC_INVALID_ADDRESS", + }); + }); + + it("throws RPC_CONTRACT_NOT_FOUND when no bytecode exists", async () => { + const getCode = vi.fn().mockResolvedValue("0x"); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { getCode, readContract: vi.fn() } }); + + const ctx = Context.create(); + ctx.param = { address: VALID_ADDRESS, chain: "local" }; + + await expect(InspectModule.inspect(ctx)).rejects.toMatchObject({ + code: "RPC_CONTRACT_NOT_FOUND", + }); + }); + + it("defaults chain to local when not provided", async () => { + const readContract = vi.fn().mockResolvedValue([]); + const getCode = vi.fn().mockResolvedValue("0x6000"); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + + const ctx = Context.create(); + ctx.param = { address: VALID_ADDRESS }; + + const result = await InspectModule.inspect(ctx); + + expect(mocks.resolveChainConfig).toHaveBeenCalledWith({ chainKey: "local" }); + expect(result.state.inspect).toMatchObject({ + success: true, + result: { chainKey: "local", facets: [] }, + }); + }); + + it("propagates RPC readContract failures", async () => { + const getCode = vi.fn().mockResolvedValue("0x6000"); + const readContract = vi.fn().mockRejectedValue(new Error("execution reverted")); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "local", rpcUrl: "https://rpc.example", chainId: 31337 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + + const ctx = Context.create(); + ctx.param = { address: VALID_ADDRESS, chain: "local" }; + + await expect(InspectModule.inspect(ctx)).rejects.toThrow("execution reverted"); + }); +}); + +describe("decodeSelector", () => { + it("decodes Diamond Loupe selectors", () => { + expect(decodeSelector("0x7a0ed627")).toBe("facets()"); + expect(decodeSelector("0x52ef6b2c")).toBe("facetAddresses()"); + expect(decodeSelector("0xadfca15e")).toBe("facetFunctionSelectors(address)"); + expect(decodeSelector("0xcdffacc6")).toBe("facetAddress(bytes4)"); + }); + + it("decodes common ERC selectors", () => { + expect(decodeSelector("0x313ce567")).toBe("decimals()"); + expect(decodeSelector("0x18160ddd")).toBe("totalSupply()"); + expect(decodeSelector("0x095ea7b3")).toBe("approve(address,uint256)"); + }); + + it("decodes Compose library selectors", () => { + expect(decodeSelector("0xf2fde38b")).toBe("transferOwnership(address)"); + expect(decodeSelector("0x8da5cb5b")).toBe("owner()"); + expect(decodeSelector("0x715018a6")).toBe("renounceOwnership()"); + }); + + it("returns raw hex for unknown selectors", () => { + expect(decodeSelector("0xdeadbeef")).toBe("0xdeadbeef"); + }); + + it("handles selectors case-insensitively", () => { + expect(decodeSelector("0x313CE567")).toBe("decimals()"); + }); +}); diff --git a/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts b/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts new file mode 100644 index 00000000..f0ae1c21 --- /dev/null +++ b/cli/test/pipelines/inspectPipeline/inspectPipeline.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; +import { Context } from "../../../src/context/context"; +import { DependencyKey } from "../../../src/resolver/dependencyKey"; + +const mocks = vi.hoisted(() => ({ + resolveChainConfig: vi.fn(), + resolve: vi.fn(), + showInspect: vi.fn(), +})); + +vi.mock("../../../src/utils/chainConfig", () => ({ resolveChainConfig: mocks.resolveChainConfig })); +vi.mock("../../../src/resolver/dependencyResolver", () => ({ DependencyResolver: { resolve: mocks.resolve } })); +vi.mock("../../../src/modules/inspect/output", () => ({ showInspect: mocks.showInspect })); + +import { InspectPipeline } from "../../../src/pipelines/inspectPipeline"; + +describe("InspectPipeline", () => { + it("delegates to InspectModule with valid address", async () => { + const readContract = vi.fn().mockResolvedValue([ + { facet: "0x0000000000000000000000000000000000000002", functionSelectors: ["0x313ce567"] }, + ]); + const getCode = vi.fn().mockResolvedValue("0x6000"); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { readContract, getCode } }); + + const ctx = Context.create(); + ctx.param = { + address: "0x0000000000000000000000000000000000000001", + chain: "sepolia", + }; + + const result = await InspectPipeline.execute(ctx); + + expect(mocks.resolve).toHaveBeenCalledWith([{ + key: DependencyKey.RPC, + params: { chainKey: "sepolia" }, + }]); + expect(result.state.inspect).toMatchObject({ + success: true, + result: { chainKey: "sepolia", chainId: 11155111 }, + }); + expect(mocks.showInspect).toHaveBeenCalledOnce(); + }); +}); diff --git a/cli/test/pipelines/rpcPipeline/rpcPipeline.test.ts b/cli/test/pipelines/rpcPipeline/rpcPipeline.test.ts new file mode 100644 index 00000000..798a5732 --- /dev/null +++ b/cli/test/pipelines/rpcPipeline/rpcPipeline.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; +import { Context } from "../../../src/context/context"; +import { DependencyKey } from "../../../src/resolver/dependencyKey"; + +const mocks = vi.hoisted(() => ({ + resolveChainConfig: vi.fn(), + resolve: vi.fn(), + showRPCCheck: vi.fn(), +})); + +vi.mock("../../../src/utils/chainConfig", () => ({ resolveChainConfig: mocks.resolveChainConfig })); +vi.mock("../../../src/resolver/dependencyResolver", () => ({ DependencyResolver: { resolve: mocks.resolve } })); +vi.mock("../../../src/modules/rpc/output", () => ({ showRPCCheck: mocks.showRPCCheck })); + +import { RPCPipeline } from "../../../src/pipelines/rpcPipeline"; + +describe("RPCPipeline", () => { + it("checks connectivity and optional contract bytecode through the resolver", async () => { + const getCode = vi.fn().mockResolvedValue("0x6000"); + mocks.resolveChainConfig.mockResolvedValue({ chainKey: "sepolia", rpcUrl: "https://rpc.example", chainId: 11155111 }); + mocks.resolve.mockResolvedValue({ [DependencyKey.RPC]: { getCode } }); + const ctx = Context.create(); + ctx.param = { + chain: "sepolia", + address: "0x0000000000000000000000000000000000000001", + }; + + const result = await RPCPipeline.execute(ctx); + + expect(mocks.resolve).toHaveBeenCalledWith([{ + key: DependencyKey.RPC, + params: { chainKey: "sepolia" }, + }]); + expect(getCode).toHaveBeenCalledWith("0x0000000000000000000000000000000000000001"); + expect(result.state.rpcCheck).toMatchObject({ + success: true, + result: { chainKey: "sepolia", chainId: 11155111, hasCode: true }, + }); + expect(mocks.showRPCCheck).toHaveBeenCalledOnce(); + }); +}); diff --git a/cli/test/resolver/dependencyResolver.test.ts b/cli/test/resolver/dependencyResolver.test.ts new file mode 100644 index 00000000..050ec941 --- /dev/null +++ b/cli/test/resolver/dependencyResolver.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { DependencyResolver } from "../../src/resolver/dependencyResolver"; +import { DependencyKey } from "../../src/resolver/dependencyKey"; + +describe("DependencyResolver RPC dependency", () => { + it("is registered and resolves chain configuration before connecting", async () => { + await expect(DependencyResolver.resolve([{ key: DependencyKey.RPC, params: { projectRoot: "/path/that/does/not/exist" } }])).rejects.toMatchObject({ + code: "RPC_INVALID_CONFIGURATION", + }); + }); +}); diff --git a/cli/test/utils/chainConfig.test.ts b/cli/test/utils/chainConfig.test.ts new file mode 100644 index 00000000..7ac1110a --- /dev/null +++ b/cli/test/utils/chainConfig.test.ts @@ -0,0 +1,70 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { RPCAdapterError } from "../../src/adapters/rpc/errors"; +import { resolveChainConfig } from "../../src/utils/chainConfig"; + +const temporaryDirectories: string[] = []; + +async function projectWithCompose(compose: unknown): Promise { + const root = await mkdtemp(path.join(os.tmpdir(), "compose-chain-")); + temporaryDirectories.push(root); + await writeFile(path.join(root, "compose.json"), JSON.stringify(compose), "utf8"); + return root; +} + +afterEach(async () => { + delete process.env.TEST_RPC_URL; + delete process.env.TEST_RPC_HOST; + delete process.env.TEST_RPC_KEY; + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); +}); + +describe("resolveChainConfig", () => { + it("defaults to the local chain", async () => { + const root = await projectWithCompose({ chains: { local: { rpc: "http://127.0.0.1:8545", chainId: 31337 } } }); + + await expect(resolveChainConfig({ projectRoot: root })).resolves.toEqual({ + chainKey: "local", + rpcUrl: "http://127.0.0.1:8545", + chainId: 31337, + }); + }); + + it("interpolates full and embedded environment variables", async () => { + process.env.TEST_RPC_URL = "https://example.test/rpc"; + process.env.TEST_RPC_HOST = "example.test"; + process.env.TEST_RPC_KEY = "secret"; + const root = await projectWithCompose({ + chains: { + full: { rpc: "${TEST_RPC_URL}", chainId: 1 }, + embedded: { rpc: "https://${TEST_RPC_HOST}/v1/${TEST_RPC_KEY}", chainId: 2 }, + }, + }); + + await expect(resolveChainConfig({ chainKey: "full", projectRoot: root })).resolves.toMatchObject({ rpcUrl: "https://example.test/rpc" }); + await expect(resolveChainConfig({ chainKey: "embedded", projectRoot: root })).resolves.toMatchObject({ rpcUrl: "https://example.test/v1/secret" }); + }); + + it("fails when an interpolated variable is missing", async () => { + const root = await projectWithCompose({ chains: { sepolia: { rpc: "${TEST_RPC_URL}", chainId: 11155111 } } }); + + await expect(resolveChainConfig({ chainKey: "sepolia", projectRoot: root })).rejects.toMatchObject>({ + code: "RPC_ENV_VAR_MISSING", + }); + }); + + it("fails when the default local chain is absent", async () => { + const root = await projectWithCompose({ chains: { sepolia: { rpc: "https://example.test", chainId: 11155111 } } }); + + await expect(resolveChainConfig({ projectRoot: root })).rejects.toMatchObject>({ code: "RPC_CHAIN_NOT_FOUND" }); + }); + + it("rejects invalid chain keys and chain definitions", async () => { + const root = await projectWithCompose({ chains: { local: { rpc: "https://example.test", chainId: 31337 } } }); + + await expect(resolveChainConfig({ chainKey: null, projectRoot: root })).rejects.toMatchObject>({ code: "RPC_INVALID_CONFIGURATION" }); + await expect(resolveChainConfig({ chainKey: "missing", projectRoot: root })).rejects.toMatchObject>({ code: "RPC_CHAIN_NOT_FOUND" }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 145c385d..22d3283f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@docusaurus/core": "^3.10.1" }, "devDependencies": { - "@changesets/cli": "^2.30.0" + "@changesets/cli": "^3.0.0" } }, "cli": { @@ -32,6 +32,7 @@ "@inquirer/select": "5.2.1", "@perfect-abstractions/compose": "0.0.4", "commander": "^13.1.0", + "dotenv": "^16.4.7", "fs-extra": "^11.3.3", "picocolors": "^1.1.1", "viem": "^2.52.2" @@ -426,12 +427,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@antfu/install-pkg/node_modules/package-manager-detector": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", - "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2167,245 +2162,282 @@ "license": "MIT" }, "node_modules/@changesets/apply-release-plan": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", - "integrity": "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.0.0.tgz", + "integrity": "sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/config": "^3.1.4", - "@changesets/get-version-range-type": "^0.4.0", - "@changesets/git": "^3.0.4", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "detect-indent": "^6.0.0", - "fs-extra": "^7.0.1", - "lodash.startcase": "^4.4.0", - "outdent": "^0.5.0", - "prettier": "^2.7.1", - "resolve-from": "^5.0.0", - "semver": "^7.5.3" + "@changesets/config": "^4.0.0", + "@changesets/format": "^0.1.1", + "@changesets/git": "^4.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/assemble-release-plan": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.10.tgz", - "integrity": "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.4", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "semver": "^7.5.3" + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/changelog-git": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", - "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/types": "^6.1.0" + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/cli": { - "version": "2.31.1", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.1.tgz", - "integrity": "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@changesets/apply-release-plan": "^7.1.1", - "@changesets/assemble-release-plan": "^6.0.10", - "@changesets/changelog-git": "^0.2.1", - "@changesets/config": "^3.1.4", - "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.4", - "@changesets/get-release-plan": "^4.0.16", - "@changesets/git": "^3.0.4", - "@changesets/logger": "^0.1.1", - "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.7", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@changesets/write": "^0.4.0", - "@inquirer/external-editor": "^1.0.2", - "@manypkg/get-packages": "^1.1.3", - "ansi-colors": "^4.1.3", - "enquirer": "^2.4.1", - "fs-extra": "^7.0.1", - "mri": "^1.2.0", - "package-manager-detector": "^0.2.0", - "picocolors": "^1.1.0", - "resolve-from": "^5.0.0", - "semver": "^7.5.3", - "spawndamnit": "^3.0.1", - "term-size": "^2.1.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.0.tgz", + "integrity": "sha512-V7Gm+GP5OT3mJinMI2YcJD/JyO/a4WChnaMCCzTRhWHgu1zhtsyY7zCjP6n5W6zsgSjJKs+yPmvtREvE0F2g8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^8.0.0", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.0", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.0", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.0", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.0" }, "bin": { "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/cli/node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, "node_modules/@changesets/config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.4.tgz", - "integrity": "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.0.tgz", + "integrity": "sha512-mw95/YrkOuhZZxfnVAA4bSXOFUi+KlhzOBTM8C4x777NhUU6HWIl9Z+K+nME+E4PVsv5NQVQwTfiHihAS1A/ow==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.4", - "@changesets/logger": "^0.1.1", - "@changesets/should-skip-package": "^0.1.2", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "fs-extra": "^7.0.1", - "micromatch": "^4.0.8" + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, - "node_modules/@changesets/errors": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", - "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "node_modules/@changesets/config/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "dependencies": { - "extendable-error": "^0.1.5" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@changesets/get-dependents-graph": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.4.tgz", - "integrity": "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==", + "node_modules/@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", "dev": true, "license": "MIT", - "dependencies": { - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "picocolors": "^1.1.0", - "semver": "^7.5.3" + "engines": { + "node": "^22.11 || ^24 || >=26" } }, - "node_modules/@changesets/get-release-plan": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.16.tgz", - "integrity": "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==", + "node_modules/@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/assemble-release-plan": "^6.0.10", - "@changesets/config": "^3.1.4", - "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.7", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3" + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, - "node_modules/@changesets/get-version-range-type": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", - "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } }, "node_modules/@changesets/git": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", - "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.0.tgz", + "integrity": "sha512-uIEswpPUgzBBqrC0qg13byNaPorzhf05LE2T+gRizEKCXvMsJC6NPJp5iNDSV/gYj4Viqh09UiOF5E7XWeH5lQ==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/errors": "^0.2.0", - "@manypkg/get-packages": "^1.1.3", - "is-subdir": "^1.1.1", - "micromatch": "^4.0.8", - "spawndamnit": "^3.0.1" + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, - "node_modules/@changesets/logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", - "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "node_modules/@changesets/git/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "dependencies": { - "picocolors": "^1.1.0" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@changesets/parse": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", - "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/types": "^6.1.0", - "js-yaml": "^4.1.1" + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/pre": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", - "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/errors": "^0.2.0", - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3", - "fs-extra": "^7.0.1" + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/read": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz", - "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.0.tgz", + "integrity": "sha512-8TdE2PwG6yArPt5Ozej83Z6iHz1G8BDBKvIEKZ455MccR4K3GNbLR2XqjBxa+5yLFnEd3xAOPXYboBg1pt8stQ==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/git": "^3.0.4", - "@changesets/logger": "^0.1.1", - "@changesets/parse": "^0.4.3", - "@changesets/types": "^6.1.0", - "fs-extra": "^7.0.1", - "p-filter": "^2.1.0", - "picocolors": "^1.1.0" + "@changesets/git": "^4.0.0", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/should-skip-package": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", - "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/types": "^6.1.0", - "@manypkg/get-packages": "^1.1.3" + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@changesets/types": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", - "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } }, "node_modules/@changesets/write": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", - "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.0.tgz", + "integrity": "sha512-xCK3/4C7Z7muQB/wguE6HcbjtY9iOtaK9orZKE+7xi573cMGD8rLZz7qlo6IvK7s+SIUIKajzj1l+F1QuP97tw==", "dev": true, "license": "MIT", "dependencies": { - "@changesets/types": "^6.1.0", - "fs-extra": "^7.0.1", - "human-id": "^4.1.1", - "prettier": "^2.7.1" + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" } }, "node_modules/@chevrotain/types": { @@ -2414,6 +2446,36 @@ "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -5827,28 +5889,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@inquirer/figures": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", @@ -6420,75 +6460,45 @@ "license": "MIT" }, "node_modules/@manypkg/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@types/node": "^12.7.1", - "find-up": "^4.1.0", - "fs-extra": "^8.1.0" - } - }, - "node_modules/@manypkg/find-root/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@manypkg/find-root/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "@manypkg/tools": "^2.1.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=20.0.0" } }, "node_modules/@manypkg/get-packages": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", - "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "@changesets/types": "^4.0.1", - "@manypkg/find-root": "^1.1.0", - "fs-extra": "^8.1.0", - "globby": "^11.0.0", - "read-yaml-file": "^1.1.0" + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", - "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@manypkg/get-packages/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=20.0.0" } }, "node_modules/@mdx-js/mdx": { @@ -6909,6 +6919,19 @@ "node": ">=12.22.0" } }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", @@ -10167,16 +10190,6 @@ "node": ">=8" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -10568,19 +10581,6 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "license": "MIT" }, - "node_modules/better-path-resolve": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", - "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-windows": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -11106,13 +11106,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", - "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", - "dev": true, - "license": "MIT" - }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -13070,16 +13063,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -13316,6 +13299,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -13416,20 +13411,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -13897,20 +13878,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -14281,13 +14248,6 @@ "node": ">=0.10.0" } }, - "node_modules/extendable-error": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", - "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -14564,20 +14524,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-yarn-workspace-root": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", @@ -14704,21 +14650,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -15551,23 +15482,6 @@ "node": ">=10.18" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -16028,35 +15942,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-subdir": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", - "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "better-path-resolve": "1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -16209,6 +16100,13 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, "node_modules/joi": { "version": "17.13.4", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", @@ -16336,15 +16234,12 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } + "license": "MIT" }, "node_modules/jsonify": { "version": "0.0.1", @@ -16790,19 +16685,6 @@ "node": ">=8.9.0" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -16827,13 +16709,6 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "license": "MIT" }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -19497,16 +19372,6 @@ "ufo": "^1.6.3" } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -20011,13 +19876,6 @@ "node": ">= 0.8.0" } }, - "node_modules/outdent": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", - "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", - "dev": true, - "license": "MIT" - }, "node_modules/ox": { "version": "0.14.33", "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", @@ -20075,29 +19933,6 @@ "node": ">=12.20" } }, - "node_modules/p-filter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", - "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-map": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-filter/node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -20107,35 +19942,6 @@ "node": ">=4" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -20196,16 +20002,6 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", @@ -20225,14 +20021,10 @@ } }, "node_modules/package-manager-detector": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", - "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "quansync": "^0.2.7" - } + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" }, "node_modules/pako": { "version": "0.2.9", @@ -20567,16 +20359,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -22296,22 +22078,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -22489,23 +22255,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -22756,46 +22505,6 @@ "react": ">=15" } }, - "node_modules/read-yaml-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", - "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.5", - "js-yaml": "^3.6.1", - "pify": "^4.0.1", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/read-yaml-file/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/read-yaml-file/node_modules/js-yaml": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", - "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -24152,30 +23861,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/spawndamnit": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", - "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "cross-spawn": "^7.0.5", - "signal-exit": "^4.0.1" - } - }, - "node_modules/spawndamnit/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -24206,13 +23891,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", @@ -24357,16 +24035,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -24651,19 +24319,6 @@ "streamx": "^2.12.5" } }, - "node_modules/term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser": { "version": "5.49.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", @@ -25466,16 +25121,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 8a75ed26..b461f21a 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "url": "https://github.com/Perfect-Abstractions/Compose.git" }, "devDependencies": { - "@changesets/cli": "^2.30.0" + "@changesets/cli": "^3.0.0" }, "dependencies": { "@docusaurus/core": "^3.10.1"