diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..aa5febb --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "develop", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/perky-rooms-tan.md b/.changeset/perky-rooms-tan.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/perky-rooms-tan.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index d0c24e9..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - ignorePatterns: ['node_modules/'], - rules: { - // Other rules... - "@typescript-eslint/no-var-requires": "off", - 'no-constant-condition': 'off', - "@typescript-eslint/no-explicit-any": "warn", - '@typescript-eslint/no-unused-vars': [ - 'error', // or 'off' to disable entirely - { - argsIgnorePattern: '^_', // Ignore unused function arguments that start with an underscore - varsIgnorePattern: '^_' // Ignore unused variables that start with an underscore - } - ], - } -}; diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml new file mode 100644 index 0000000..2564fc1 --- /dev/null +++ b/.github/workflows/changeset-check.yml @@ -0,0 +1,36 @@ +name: Changeset Check + +on: + pull_request: + branches: [master, develop] + +jobs: + changeset: + name: Require changeset + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check for changeset + run: | + echo "Checking for changeset files in this PR..." + echo "If this fails, run 'pnpm changeset' to add a changeset describing your changes." + echo "For changes that don't need a changelog entry (docs, CI, refactoring), use 'pnpm changeset --empty'." + pnpm changeset status --since=origin/${{ github.base_ref }} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c2cf56a..4e5e785 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,13 +16,30 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" + - name: Get pnpm store directory + id: pnpm-cache + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies - run: npm install -g pnpm && pnpm i + run: pnpm install --frozen-lockfile - name: Linting run: pnpm lint diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 639c434..48d4332 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,6 +21,17 @@ jobs: - name: Install dependencies run: npm install -g pnpm && pnpm i + - name: Verify changesets are consumed + if: startsWith(github.ref, 'refs/tags/') + run: | + pnpm changeset version + if ! git diff --quiet; then + echo "Error: 'pnpm changeset version' produced uncommitted changes on a tag build." + echo "Please run 'pnpm changeset version' locally, commit the resulting changes, and re-create the tag." + git diff + exit 1 + fi + - name: Build run: pnpm build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4004561..bf63058 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,8 +50,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run tests - run: pnpm test + - name: Run tests with coverage threshold + run: pnpm test:ci - name: Build project run: pnpm build diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..71e051f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +pnpm lint-staged +pnpm typecheck diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4cc008c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# @offckb/cli + +## Changelog + +All notable changes to this project will be documented in this file. +This file is automatically updated by [changesets](https://github.com/changesets/changesets). diff --git a/README.md b/README.md index 164f98c..d575e6d 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,38 @@ offckb system-scripts --output By default, OffCKB use a fixed Devnet config. You can customize it, for example by modifying the default log level (`warn,ckb-script=debug`). -1. Locate your Devnet config folder: - +1. Open the interactive Devnet config editor: + +```sh +offckb devnet config +``` + +The editor uses a three-column layout: first-column file switcher (`ckb.toml` / `ckb-miner.toml`), a middle primary editing pane, and a smaller right read-only reference pane that shows the full built-in template for the currently selected file. + +The left editing pane supports full key browsing/editing, including primitive value edits, object key add, array append/insert/move, search filter, and path delete. + +Common shortcuts: `Enter` edit primitive, `a` add key/item, `i` insert array item, `m` move array item, `d` delete path, `/` search filter, `n`/`N` next/previous search match, `c` add custom value in fixed-array dialog (when allowed), `s` save, `q` quit. + +Note: saving rewrites `ckb.toml` / `ckb-miner.toml` into canonical TOML format; upstream comments and original formatting are not preserved after save. + +You can also update the same fields non-interactively (useful for scripts/CI): + +```sh +offckb devnet config --set ckb.logger.filter=info +offckb devnet config --set ckb.rpc.enable_deprecated_rpc=true --set miner.client.poll_interval=1500 +``` + +If your terminal is non-interactive (no TTY, e.g. CI/remote pipeline), use `--set` mode directly instead of the full-screen editor. + +1. Save changes and restart devnet: + +```sh +offckb clean -d +offckb node +``` + +1. (Advanced) Locate your Devnet config folder for manual edits: + ```sh offckb config list ``` @@ -293,12 +323,12 @@ Example result: } } ``` + Pay attention to the `devnet.configPath` and `devnet.dataPath`. - -2. `cd` into the `devnet.configPath` . Modify the config files as needed. See [Custom Devnet Setup](https://docs.nervos.org/docs/node/run-devnet-node#custom-devnet-setup) and [Configure CKB](https://github.com/nervosnetwork/ckb/blob/develop/docs/configure.md) for details. -3. After modifications, run `offckb clean -d` to remove the chain data if needed while keeping the updated config files. -4. Restart local blockchain by running `offckb node` +1. `cd` into the `devnet.configPath` . Modify the config files as needed. See [Custom Devnet Setup](https://docs.nervos.org/docs/node/run-devnet-node#custom-devnet-setup) and [Configure CKB](https://github.com/nervosnetwork/ckb/blob/develop/docs/configure.md) for details. +2. After modifications, run `offckb clean -d` to remove the chain data if needed while keeping the updated config files. +3. Restart local blockchain by running `offckb node` ## Config Setting diff --git a/docs/develop.md b/docs/develop.md index bf9657c..e8048d3 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -1,39 +1,300 @@ -## Development +# Development Guide -### Update built-in scripts +This is the **single source of truth** for all development workflows in OffCKB. -required +## Table of Contents +- [Development Guide](#development-guide) + - [Table of Contents](#table-of-contents) + - [Local Development Setup](#local-development-setup) + - [Prerequisites](#prerequisites) + - [Clone \& Install](#clone--install) + - [Run in Development Mode](#run-in-development-mode) + - [Build for Production](#build-for-production) + - [Common Commands](#common-commands) + - [Code Architecture](#code-architecture) + - [Branch Management](#branch-management) + - [PR Workflow](#pr-workflow) + - [Changesets (Changelog)](#changesets-changelog) + - [When to create a changeset](#when-to-create-a-changeset) + - [How to create a changeset](#how-to-create-a-changeset) + - [Empty changesets](#empty-changesets) + - [How changesets are consumed](#how-changesets-are-consumed) + - [Release Process](#release-process) + - [Standard Release (to npm `latest`)](#standard-release-to-npm-latest) + - [Canary Release (to npm `canary`)](#canary-release-to-npm-canary) + - [Testing](#testing) + - [Conventions](#conventions) + - [What to test](#what-to-test) + - [Coverage](#coverage) + - [Updating Built-in CKB Scripts](#updating-built-in-ckb-scripts) + - [Updating Chain Config](#updating-chain-config) + - [Updating Templates](#updating-templates) + - [Updating CKB WASM Debugger](#updating-ckb-wasm-debugger) + +--- + +## Local Development Setup + +### Prerequisites + +- Node.js >= 20.0.0 +- pnpm (install via `npm install -g pnpm`) + +### Clone & Install + +```sh +git clone --recurse-submodules https://github.com/ckb-devrel/offckb.git +cd offckb +pnpm install +``` + +> If you already cloned without `--recurse-submodules`, run `git submodule update --init --recursive`. + +### Run in Development Mode + +```sh +pnpm start +# This runs ts-node-dev with hot-reload on src/cli.ts +``` + +### Build for Production + +```sh +pnpm build +# tsc → ncc bundle → build/index.js +``` + +--- + +## Common Commands + +| Command | Description | +|-----------------------|----------------------------------------------| +| `pnpm start` | Run CLI in dev mode (ts-node-dev, hot-reload)| +| `pnpm build` | Build production bundle to `build/` | +| `pnpm lint` | Run ESLint on `src/**/*.ts` | +| `pnpm lint:fix` | Run ESLint with auto-fix | +| `pnpm fmt` | Format code with Prettier | +| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | +| `pnpm test` | Run unit tests (Jest) | +| `pnpm test:watch` | Run tests in watch mode | +| `pnpm test:coverage` | Run tests with coverage report | +| `pnpm test:ci` | Run tests with coverage (CI mode) | +| `pnpm changeset` | Create a changeset file for your PR | +| `pnpm clean` | Remove `dist/`, `build/`, `target/` | + +--- + +## Code Architecture + +**Key directories:** + +| Directory | Role | +|-----------|------| +| `src/cli.ts` | Entry point — registers all CLI commands via `commander` | +| `src/cmd/` | Command implementations, one file per CLI command (e.g. `node.ts`, `create.ts`, `deploy.ts`) | +| `src/cfg/` | Configuration: accounts, environment paths, settings | +| `src/sdk/` | CKB SDK wrappers (RPC calls, network utilities) | +| `src/deploy/` | Contract deployment logic (migration, script handling, TOML generation) | +| `src/tools/` | WASM debugger, RPC proxy, transaction dumper | +| `src/tui/` | Terminal UI (blessed-based devnet config editor) | +| `src/templates/` | Project scaffolding template processing | +| `src/util/` | Shared utilities (fs, encoding, logger, validator, etc.) | +| `src/type/` | Shared TypeScript type definitions | +| `ckb/` | Git submodules for CKB smart contract source code, built via `Makefile` | +| `templates/v4/` | Project scaffolding templates shipped with the CLI | +| `build/` | ncc-bundled production output — **never edit directly** | + +**Conventions:** +- Adding a new CLI command: create `src/cmd/.ts`, register it in `src/cli.ts` +- `src/cmd/*` modules should use `src/sdk/` for CKB interaction and `src/util/` for helpers +- Keep `src/type/base.ts` for shared types; command-specific types stay in command files +- Native CKB script binaries live in `ckb/` submodules — modify via `Makefile`, not directly + +--- + +## Branch Management + +| Branch | Purpose | CI | +|--------|---------|----| +| `develop` | Development mainline. All feature/fix branches merge here first. Version is kept up-to-date with bumps. | lint + test (matrix) | +| `master` | Stable release branch. Receives merges from `develop` for formal releases. | lint + test + publish on tag | +| `feature/*` | New feature work. Branch from `develop`, merge back to `develop`. | lint + test | +| `fix/*` | Bug fixes. Branch from `develop`, merge back to `develop`. | lint + test | +| `v0.*.x` | Canary / maintenance branches. Pushes auto-publish canary releases to npm. | canary publish | + +**Normal flow — all changes go through `develop` first:** +``` +feature/* ──→ develop ──→ v0.*.x ──→ canary publish (npm canary tag) + └──→ master ──→ tag v*.*.* ──→ npm publish (latest) +``` + +**Exception — direct commits to `v0.*.x` (skip develop):** +Only for: +- Hotfix that needs immediate canary release +- Legacy/incompatible branches (e.g. v2, v3) that diverge from current `develop` + +``` +fix/* ──→ v0.*.x (direct) ──→ canary publish +``` + +--- + +## PR Workflow + +1. Create a branch from `develop` (e.g. `feature/add-foo` or `fix/bar-crash`) +2. Make your changes +3. Run `pnpm changeset` to generate a changeset file describing the change (**required by CI**) + - For changes that don't need a changelog entry (docs, CI config, pure refactoring), use `pnpm changeset --empty` +4. Commit and push. Pre-commit hooks will automatically run: + - ESLint (with auto-fix) + Prettier on staged `.ts` files + - Prettier on staged template/account files + - TypeScript type check (`tsc --noEmit`) +5. Open a PR to `develop` (or `master` for hotfix) +6. CI must pass: + - **Nodejs CI**: lint + build + format consistency check + - **Test**: unit tests + integration tests (Ubuntu/Windows/macOS) + - **Changeset Check**: verifies a changeset file is present +7. Get code review, then merge + +--- + +## Changesets (Changelog) + +We use [changesets](https://github.com/changesets/changesets) to manage changelog entries and version bumps. + +### When to create a changeset + +**Every PR** must include a changeset file. CI will block the PR otherwise. + +### How to create a changeset + +```sh +pnpm changeset +``` + +This interactive CLI will ask you: +1. **Version bump type**: `patch` (bug fix), `minor` (new feature), `major` (breaking change) +2. **Summary**: A human-readable description of the change (this goes into CHANGELOG.md) + +The command creates a markdown file in `.changeset/` — commit it with your PR. + +### Empty changesets + +For PRs that don't affect the published package (documentation, CI, internal refactoring): + +```sh +pnpm changeset --empty +``` + +This creates a changeset that satisfies CI but won't add a CHANGELOG entry or bump the version. + +### How changesets are consumed + +During the [release process](#release-process), `pnpm changeset version` reads all accumulated changeset files, determines the version bump, updates `package.json` version and `CHANGELOG.md`, then deletes the consumed changeset files. + +--- + +## Release Process + +### Standard Release (to npm `latest`) + +1. Ensure `develop` is stable and all target PRs are merged +2. On `develop`, run changeset version to consume accumulated changesets: + ```sh + pnpm changeset version + ``` + This consumes all `.changeset/*.md` files, updates `CHANGELOG.md` and bumps `package.json` version +3. Commit the version bump: + ```sh + git add . + git commit -m "chore: release v$(node -p 'require(\"./package.json\").version')" + ``` +4. Merge `develop` into `master` +5. On `master`, create and push the tag: + ```sh + git tag v$(node -p 'require("./package.json").version') + git push origin master --tags + ``` +6. The `Publish on Tag` workflow automatically publishes to npm + +### Canary Release (to npm `canary`) + +Merge `develop` into the target `v0.*.x` branch (or push directly for hotfix/legacy cases). The `Canary release on new commit` workflow will: +- Auto-bump version with a canary prerelease suffix (e.g. `0.4.5-canary-abc1234`) +- Publish to npm under the `canary` tag + +> **Note:** Direct commits to `v0.*.x` (bypassing develop) should only be used for hotfixes or legacy branches that are incompatible with current `develop`. + +--- + +## Testing + +### Conventions + +- Test files: `*.test.ts` +- Location: `tests/` directory (or `src/**/__tests__/` for co-located tests) +- Framework: Jest with ts-jest +- Run: `pnpm test` + +### What to test + +- **New features/fixes must include tests** +- Priority for coverage improvement: + 1. Pure logic modules: `src/util/`, `src/cfg/`, `src/scripts/`, `src/deploy/` + 2. Command logic: `src/cmd/*` (mock CKB node / file system as needed) + 3. Template processing: `src/templates/` +- Integration tests: `scripts/create-test.sh` (runs full create → build → test cycle) + +### Coverage + +- CI enforces a minimum coverage threshold (currently 10% statements) +- The threshold will be raised as tests are added +- View coverage locally: `pnpm test:coverage` then open `coverage/index.html` +- Coverage is uploaded to Codecov on CI (Ubuntu only) + +--- + +## Updating Built-in CKB Scripts + +**Prerequisites:** - rust/cargo -- capsule https://github.com/nervosnetwork/capsule/releases +- [capsule](https://github.com/nervosnetwork/capsule/releases) - docker -update submodule inside `ckb` and then run +Update the relevant submodule inside `ckb/`, then run: ```sh make all ``` -### Update chain config +See `Makefile` for individual targets: `omnilock`, `anyone-can-pay`, `xudt`, `spore`, `ckb-js-vm`, `nostr-lock`, `pw-lock`, `secp256k1_multisig_v2`. + +--- + +## Updating Chain Config -edit the things in `ckb/devnet` +Edit files in `ckb/devnet/`. -All the script configs are generated by `ckb list-hashes` so you don't need to care about it. +All script configs are generated by `ckb list-hashes` — you don't need to manually maintain script hashes. -### Update templates +--- -edit the things in `templates/v${version}` +## Updating Templates -## Update CKB wasm debugger +Edit files in `templates/v4/`. Template files use `.template` extension and are processed by `src/templates/processor.ts`. +--- -required +## Updating CKB WASM Debugger +**Prerequisites:** - rust/cargo -- add wasm32-wasip1 target via `rustup target add wasm32-wasip1` -- on linux, `sudo apt install gcc-multilib` +- wasm32-wasip1 target: `rustup target add wasm32-wasip1` +- On Linux: `sudo apt install gcc-multilib` -update the `ckb-standalone-debugger` submodule and run: +Update the `ckb-standalone-debugger` submodule, then run: ```sh make ckb-debugger diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..a7b0904 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,34 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; + +export default [ + { + ignores: ['node_modules/', 'build/', 'dist/', '*.js', '!eslint.config.mjs'], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': typescriptEslint, + }, + rules: { + ...typescriptEslint.configs.recommended.rules, + '@typescript-eslint/no-var-requires': 'off', + 'no-constant-condition': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }, +]; diff --git a/jest.config.js b/jest.config.js index 6e48210..07a73c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,11 @@ module.exports = { ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], + coverageThreshold: { + global: { + statements: 10, + }, + }, moduleNameMapper: { '^@/(.*)$': '/src/$1', }, diff --git a/package.json b/package.json index c5df859..6b5b14a 100644 --- a/package.json +++ b/package.json @@ -39,24 +39,27 @@ "fmt": "prettier --write '{src,templates,account}/**/*.{js,jsx,ts,tsx,md,json}'", "test": "jest", "test:watch": "jest --watch", - "test:coverage": "jest --coverage" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "test:coverage": "jest --coverage", + "test:ci": "jest --coverage --ci", + "typecheck": "tsc --noEmit", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "changeset publish", + "prepare": "husky" }, "lint-staged": { - "{src,templates,account}/**/*.{js,jsx,ts,tsx,md,json}": "prettier --ignore-unknown --write" - }, - "eslintConfig": { - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" + "src/**/*.ts": [ + "eslint --fix", + "prettier --write" + ], + "{templates,account}/**/*.{js,jsx,ts,tsx,md,json}": [ + "prettier --ignore-unknown --write" ] }, "devDependencies": { + "@changesets/cli": "^2.29.8", "@types/adm-zip": "^0.5.5", + "@types/blessed": "0.1.27", "@types/jest": "^30.0.0", "@types/node": "^20.17.24", "@types/node-fetch": "^2.6.11", @@ -65,7 +68,7 @@ "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vercel/ncc": "^0.38.3", - "eslint": "^8.57.0", + "eslint": "^9.26.0", "husky": "^9.0.11", "jest": "^30.2.0", "lint-staged": "^15.2.2", @@ -80,6 +83,7 @@ "@inquirer/prompts": "^7.8.6", "@types/http-proxy": "^1.17.15", "adm-zip": "^0.5.10", + "blessed": "0.1.81", "chalk": "4.1.2", "child_process": "^1.0.2", "ckb-transaction-dumper": "^0.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84219af..86cced1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@ckb-ccc/core': specifier: 1.5.3 - version: 1.5.3(typescript@5.8.2) + version: 1.5.3(typescript@5.8.2)(zod@3.25.76) '@iarna/toml': specifier: ^2.2.5 version: 2.2.5 @@ -23,6 +23,9 @@ importers: adm-zip: specifier: ^0.5.10 version: 0.5.16 + blessed: + specifier: 0.1.81 + version: 0.1.81 chalk: specifier: 4.1.2 version: 4.1.2 @@ -49,14 +52,20 @@ importers: version: 7.7.3 tar: specifier: ^7.5.3 - version: 7.5.7 + version: 7.5.9 winston: specifier: ^3.17.0 version: 3.17.0 devDependencies: + '@changesets/cli': + specifier: ^2.29.8 + version: 2.29.8(@types/node@20.17.24) '@types/adm-zip': specifier: ^0.5.5 version: 0.5.7 + '@types/blessed': + specifier: 0.1.27 + version: 0.1.27 '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -74,16 +83,16 @@ importers: version: 6.1.13 '@typescript-eslint/eslint-plugin': specifier: ^7.0.2 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.26.0)(typescript@5.8.2))(eslint@9.26.0)(typescript@5.8.2) '@typescript-eslint/parser': specifier: ^7.0.2 - version: 7.18.0(eslint@8.57.1)(typescript@5.8.2) + version: 7.18.0(eslint@9.26.0)(typescript@5.8.2) '@vercel/ncc': specifier: ^0.38.3 version: 0.38.3 eslint: - specifier: ^8.57.0 - version: 8.57.1 + specifier: ^9.26.0 + version: 9.26.0 husky: specifier: ^9.0.11 version: 9.1.7 @@ -265,6 +274,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -280,6 +293,61 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@changesets/apply-release-plan@7.0.14': + resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.8': + resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==} + hasBin: true + + '@changesets/config@3.1.2': + resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.14': + resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.2': + resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.6': + resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@ckb-ccc/core@1.5.3': resolution: {integrity: sha512-/W7SYbygBateN6odqkMhQlkoQFs+45pJ7hYZYEaEpRdF6DjU7sIOvVSkw3qXiUOK37b2qAWJj3I8CJQbesKpng==} @@ -303,36 +371,65 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.26.0': + resolution: {integrity: sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} @@ -594,6 +691,22 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -680,6 +793,12 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/blessed@0.1.27': + resolution: {integrity: sha512-ZOQGjLvWDclAXp0rW5iuUBXeD6Gr1PkitN7tj7/G8FCoSzTsij6OhXusOzMKhwrZ9YlL2Pmu0d6xJ9zVvk+Hsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/http-proxy@1.17.16': resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} @@ -695,9 +814,15 @@ packages: '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.17.24': resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==} @@ -828,41 +953,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -901,6 +1034,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -915,6 +1052,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + adm-zip@0.5.16: resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} engines: {node: '>=12.0'} @@ -926,8 +1068,23 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -1012,6 +1169,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} @@ -1031,6 +1192,10 @@ packages: bech32@2.0.0: resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1045,14 +1210,24 @@ packages: resolution: {integrity: sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==} engines: {node: '>=0.10'} - bn.js@4.12.1: - resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + blessed@0.1.81: + resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==} + engines: {node: '>= 0.8.0'} + hasBin: true + + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.3: + resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1105,6 +1280,10 @@ packages: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} engines: {node: '>=10.0.0'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1158,6 +1337,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -1236,9 +1419,29 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cpu-features@0.0.10: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} @@ -1262,8 +1465,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1294,6 +1497,14 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1306,10 +1517,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - drbg.js@1.0.1: resolution: {integrity: sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==} engines: {node: '>=0.10'} @@ -1324,6 +1531,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -1346,6 +1556,14 @@ packages: enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1373,6 +1591,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -1381,31 +1602,39 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.26.0: + resolution: {integrity: sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -1420,6 +1649,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + ethers@6.13.5: resolution: {integrity: sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==} engines: {node: '>=14.0.0'} @@ -1434,6 +1667,14 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -1453,6 +1694,19 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1466,6 +1720,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1475,9 +1732,9 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -1486,6 +1743,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1494,9 +1755,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1525,6 +1786,22 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1578,15 +1855,16 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -1636,9 +1914,17 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + hono@4.12.2: + resolution: {integrity: sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==} + engines: {node: '>=16.9.0'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -1647,6 +1933,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -1664,6 +1954,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1691,6 +1985,14 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1737,9 +2039,8 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} @@ -1749,10 +2050,18 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1915,6 +2224,9 @@ packages: node-notifier: optional: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1943,6 +2255,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1951,6 +2269,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1995,6 +2316,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -2026,6 +2350,14 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2041,10 +2373,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2063,11 +2403,11 @@ packages: minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.8: + resolution: {integrity: sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -2090,6 +2430,10 @@ packages: engines: {node: '>=10'} hasBin: true + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2108,6 +2452,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -2138,6 +2486,18 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2160,6 +2520,13 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2176,6 +2543,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -2183,6 +2554,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2191,6 +2565,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2214,6 +2592,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2234,10 +2615,18 @@ packages: engines: {node: '>=0.10'} hasBin: true + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -2250,6 +2639,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -2259,6 +2653,10 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2266,12 +2664,31 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2284,6 +2701,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -2320,14 +2741,13 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2354,10 +2774,21 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} engines: {node: '>= 0.10'} @@ -2371,6 +2802,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2403,6 +2850,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2413,6 +2863,10 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2484,10 +2938,14 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} - tar@7.5.7: - resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} + tar@7.5.9: + resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==} engines: {node: '>=18'} + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -2495,9 +2953,6 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2509,6 +2964,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2595,10 +3054,6 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -2611,6 +3066,10 @@ packages: resolution: {integrity: sha512-rLjWJzQFOq4xw7MgJrCZ6T1jIOvvYElXT12r+y0CC6u67hegDHaxcPqb2fZHOGlqxugGQPNB1EnTezjBetkwkw==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2631,6 +3090,14 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -2656,6 +3123,10 @@ packages: varuint-bitcoin@1.1.2: resolution: {integrity: sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -2776,6 +3247,14 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@adraffy/ens-normalize@1.10.1': {} @@ -2801,7 +3280,7 @@ snapshots: '@babel/types': 7.28.6 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2944,6 +3423,8 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 + '@babel/runtime@7.28.6': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.28.6 @@ -2958,7 +3439,7 @@ snapshots: '@babel/parser': 7.28.6 '@babel/template': 7.28.6 '@babel/types': 7.28.6 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -2969,9 +3450,153 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@ckb-ccc/core@1.5.3(typescript@5.8.2)': + '@changesets/apply-release-plan@7.0.14': + dependencies: + '@changesets/config': 3.1.2 + '@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.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.3 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.3 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.8(@types/node@20.17.24)': + dependencies: + '@changesets/apply-release-plan': 7.0.14 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.2 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.14 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.6 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.2(@types/node@20.17.24) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.3 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': dependencies: - '@joyid/ckb': 1.1.2(typescript@5.8.2) + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.3 + + '@changesets/get-release-plan@4.0.14': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.2 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.6 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.2': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.6': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.2 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + + '@ckb-ccc/core@1.5.3(typescript@5.8.2)(zod@3.25.76)': + dependencies: + '@joyid/ckb': 1.1.2(typescript@5.8.2)(zod@3.25.76) '@noble/ciphers': 0.5.3 '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 @@ -3019,40 +3644,64 @@ snapshots: tslib: 2.7.0 optional: true - '@eslint-community/eslint-utils@4.5.1(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.26.0)': dependencies: - eslint: 8.57.1 + eslint: 9.26.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.20.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.3': {} + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 - '@eslint/eslintrc@2.1.4': + '@eslint/eslintrc@3.3.3': dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 9.6.1 - globals: 13.24.0 + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} + '@eslint/js@9.26.0': {} - '@humanwhocodes/config-array@0.13.0': + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.2.8': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@hono/node-server@1.19.9(hono@4.12.2)': + dependencies: + hono: 4.12.2 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@iarna/toml@2.2.5': {} @@ -3383,9 +4032,9 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 - '@joyid/ckb@1.1.2(typescript@5.8.2)': + '@joyid/ckb@1.1.2(typescript@5.8.2)(zod@3.25.76)': dependencies: - '@joyid/common': 0.2.1(typescript@5.8.2) + '@joyid/common': 0.2.1(typescript@5.8.2)(zod@3.25.76) '@nervosnetwork/ckb-sdk-utils': 0.109.5 cross-fetch: 4.0.0 uncrypto: 0.1.3 @@ -3394,9 +4043,9 @@ snapshots: - typescript - zod - '@joyid/common@0.2.1(typescript@5.8.2)': + '@joyid/common@0.2.1(typescript@5.8.2)(zod@3.25.76)': dependencies: - abitype: 0.8.7(typescript@5.8.2) + abitype: 0.8.7(typescript@5.8.2)(zod@3.25.76) type-fest: 4.6.0 transitivePeerDependencies: - typescript @@ -3426,6 +4075,44 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.28.6 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.28.6 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.2) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.12.2 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -3522,6 +4209,12 @@ snapshots: dependencies: '@babel/types': 7.28.6 + '@types/blessed@0.1.27': + dependencies: + '@types/node': 20.17.24 + + '@types/estree@1.0.8': {} + '@types/http-proxy@1.17.16': dependencies: '@types/node': 20.17.24 @@ -3541,11 +4234,15 @@ snapshots: expect: 30.2.0 pretty-format: 30.2.0 + '@types/json-schema@7.0.15': {} + '@types/node-fetch@2.6.12': dependencies: '@types/node': 20.17.24 form-data: 4.0.4 + '@types/node@12.20.55': {} + '@types/node@20.17.24': dependencies: undici-types: 6.19.8 @@ -3575,15 +4272,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.26.0)(typescript@5.8.2))(eslint@9.26.0)(typescript@5.8.2)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.8.2) + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@9.26.0)(typescript@5.8.2) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.8.2) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.2) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.26.0)(typescript@5.8.2) + '@typescript-eslint/utils': 7.18.0(eslint@9.26.0)(typescript@5.8.2) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 8.57.1 + eslint: 9.26.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -3593,14 +4290,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.2)': + '@typescript-eslint/parser@7.18.0(eslint@9.26.0)(typescript@5.8.2)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 - eslint: 8.57.1 + debug: 4.4.3 + eslint: 9.26.0 optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -3611,12 +4308,12 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.8.2)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.26.0)(typescript@5.8.2)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.2) - debug: 4.4.0 - eslint: 8.57.1 + '@typescript-eslint/utils': 7.18.0(eslint@9.26.0)(typescript@5.8.2) + debug: 4.4.3 + eslint: 9.26.0 ts-api-utils: 1.4.3(typescript@5.8.2) optionalDependencies: typescript: 5.8.2 @@ -3629,10 +4326,10 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.5 + minimatch: 9.0.8 semver: 7.7.3 ts-api-utils: 1.4.3(typescript@5.8.2) optionalDependencies: @@ -3640,13 +4337,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.8.2)': + '@typescript-eslint/utils@7.18.0(eslint@9.26.0)(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.26.0) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) - eslint: 8.57.1 + eslint: 9.26.0 transitivePeerDependencies: - supports-color - typescript @@ -3719,37 +4416,59 @@ snapshots: '@vercel/ncc@0.38.3': {} - abitype@0.8.7(typescript@5.8.2): + abitype@0.8.7(typescript@5.8.2)(zod@3.25.76): dependencies: typescript: 5.8.2 + optionalDependencies: + zod: 3.25.76 abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.14.1): + accepts@2.0.0: dependencies: - acorn: 8.14.1 + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.1: {} + acorn@8.15.0: {} + adm-zip@0.5.16: {} aes-js@4.0.0-beta.5: {} agent-base@7.1.3: {} - ajv@6.12.6: + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -3847,6 +4566,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base-x@3.0.11: dependencies: safe-buffer: 5.2.1 @@ -3861,6 +4582,10 @@ snapshots: bech32@2.0.0: {} + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -3880,16 +4605,32 @@ snapshots: secp256k1: 3.8.1 varuint-bitcoin: 1.1.2 - bn.js@4.12.1: {} + blessed@0.1.81: {} + + bn.js@4.12.3: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@5.0.3: dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -3955,6 +4696,8 @@ snapshots: buildcheck@0.0.6: optional: true + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4007,6 +4750,8 @@ snapshots: chownr@3.0.0: {} + ci-info@3.9.0: {} + ci-info@4.3.1: {} cipher-base@1.0.6: @@ -4078,8 +4823,21 @@ snapshots: concat-map@0.0.1: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cpu-features@0.0.10: dependencies: buildcheck: 0.0.6 @@ -4123,7 +4881,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.0: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -4141,6 +4899,10 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + + detect-indent@6.1.0: {} + detect-newline@3.1.0: {} diff@4.0.4: {} @@ -4149,10 +4911,6 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - drbg.js@1.0.1: dependencies: browserify-aes: 1.2.0 @@ -4171,11 +4929,13 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.267: {} elliptic@6.6.1: dependencies: - bn.js: 4.12.1 + bn.js: 4.12.3 brorand: 1.1.0 hash.js: 1.1.7 hmac-drbg: 1.0.1 @@ -4193,6 +4953,13 @@ snapshots: enabled@2.0.0: {} + encodeurl@2.0.0: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + environment@1.1.0: {} error-ex@1.3.4: @@ -4216,69 +4983,73 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} - eslint-scope@7.2.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.1: + eslint-visitor-keys@4.2.1: {} + + eslint@9.26.0: dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.26.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.13.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.26.0 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 + '@humanwhocodes/retry': 0.4.3 + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 - doctrine: 3.0.0 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + zod: 3.25.76 transitivePeerDependencies: + - '@cfworker/json-schema' - supports-color - espree@9.6.1: + espree@10.4.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 3.4.3 + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -4290,6 +5061,8 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + ethers@6.13.5: dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -4309,6 +5082,12 @@ snapshots: eventemitter3@5.0.1: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -4349,6 +5128,46 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -4363,6 +5182,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4373,9 +5194,9 @@ snapshots: fecha@4.2.3: {} - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 file-uri-to-path@1.0.0: {} @@ -4383,6 +5204,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -4393,11 +5225,10 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.3 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.3.3: {} @@ -4422,6 +5253,22 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4471,7 +5318,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.8 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -4481,13 +5328,11 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globby@11.1.0: dependencies: @@ -4546,8 +5391,18 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + hono@4.12.2: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 @@ -4559,10 +5414,12 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color + human-id@4.1.3: {} + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -4573,6 +5430,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4596,6 +5457,10 @@ snapshots: inherits@2.0.4: {} + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + is-arrayish@0.2.1: {} is-arrayish@0.3.2: {} @@ -4628,16 +5493,22 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} + is-promise@4.0.0: {} is-stream@2.0.1: {} is-stream@3.0.0: {} + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.19 + is-windows@1.0.2: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -4667,7 +5538,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.0 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -4995,6 +5866,8 @@ snapshots: - supports-color - ts-node + jose@6.1.3: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -5016,10 +5889,18 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5041,7 +5922,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.0 + debug: 4.4.3 execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.2.5 @@ -5073,6 +5954,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.startcase@4.4.0: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -5114,6 +5997,10 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5125,10 +6012,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -5139,13 +6032,13 @@ snapshots: minimalistic-crypto-utils@1.0.1: {} - minimatch@3.1.2: + minimatch@3.1.5: dependencies: brace-expansion: 1.1.12 - minimatch@9.0.5: + minimatch@9.0.8: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.3 minimist@1.2.8: {} @@ -5159,6 +6052,8 @@ snapshots: mkdirp@1.0.4: {} + mri@1.2.0: {} + ms@2.1.3: {} mute-stream@2.0.0: {} @@ -5169,6 +6064,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} node-fetch@2.7.0: @@ -5189,6 +6086,14 @@ snapshots: dependencies: path-key: 4.0.0 + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -5218,6 +6123,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -5234,10 +6145,16 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@2.1.0: {} + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5249,6 +6166,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -5264,6 +6183,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} picocolors@1.1.1: {} @@ -5274,8 +6195,12 @@ snapshots: pidtree@0.6.0: {} + pify@4.0.1: {} + pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -5284,6 +6209,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@2.8.8: {} + prettier@3.5.3: {} pretty-format@30.2.0: @@ -5292,14 +6219,41 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} pure-rand@7.0.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-is@18.3.1: {} + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -5312,6 +6266,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-cwd@3.0.0: @@ -5341,15 +6297,21 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - ripemd160@2.0.2: dependencies: hash-base: 3.1.0 inherits: 2.0.4 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5364,7 +6326,7 @@ snapshots: dependencies: bindings: 1.5.0 bip66: 1.1.5 - bn.js: 4.12.1 + bn.js: 4.12.3 create-hash: 1.2.0 drbg.js: 1.0.1 elliptic: 6.6.1 @@ -5375,6 +6337,31 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5384,6 +6371,8 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + sha.js@2.4.12: dependencies: inherits: 2.0.4 @@ -5396,6 +6385,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -5428,6 +6445,11 @@ snapshots: source-map@0.6.1: {} + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + sprintf-js@1.0.3: {} stack-trace@0.0.10: {} @@ -5436,6 +6458,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + statuses@2.0.2: {} + string-argv@0.3.2: {} string-length@4.0.2: @@ -5499,7 +6523,7 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tar@7.5.7: + tar@7.5.9: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -5507,16 +6531,16 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + term-size@2.2.1: {} + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 text-hex@1.0.0: {} - text-table@0.2.0: {} - tmpl@1.0.5: {} to-buffer@1.2.2: @@ -5529,6 +6553,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tr46@0.0.3: {} tree-kill@1.2.2: {} @@ -5612,14 +6638,18 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@4.41.0: {} type-fest@4.6.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5635,6 +6665,10 @@ snapshots: undici-types@6.19.8: {} + universalify@0.1.2: {} + + unpipe@1.0.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -5683,6 +6717,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + vary@1.1.2: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -5794,3 +6830,9 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/src/cli.ts b/src/cli.ts index 5a24f9a..dc82791 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,6 +10,7 @@ import { TransferOptions, transfer } from './cmd/transfer'; import { BalanceOption, balanceOf } from './cmd/balance'; import { createScriptProject, CreateScriptProjectOptions } from './cmd/create'; import { Config, ConfigItem } from './cmd/config'; +import { devnetConfig } from './cmd/devnet-config'; import { debugSingleScript, debugTransaction, parseSingleScriptOption } from './cmd/debug'; import { printSystemScripts } from './cmd/system-scripts'; import { transferAll } from './cmd/transfer-all'; @@ -164,6 +165,19 @@ program .description('do a configuration action') .action((action, item, value) => Config(action, item as ConfigItem, value)); +const devnetCommand = program.command('devnet').description('Devnet utility commands'); + +devnetCommand + .command('config') + .description('Open a full-screen editor to tweak devnet config files') + .option( + '-s, --set ', + 'Set a devnet config field non-interactively (repeatable), e.g. --set ckb.logger.filter=info', + (value: string, previous: string[] = []) => [...previous, value], + [], + ) + .action(devnetConfig); + program.parse(process.argv); // If no command is specified, display help diff --git a/src/cmd/devnet-config.ts b/src/cmd/devnet-config.ts new file mode 100644 index 0000000..cc8b5d3 --- /dev/null +++ b/src/cmd/devnet-config.ts @@ -0,0 +1,79 @@ +import { readSettings } from '../cfg/setting'; +import { logger } from '../util/logger'; +import { createDevnetConfigEditor } from '../devnet/config-editor'; +import { runDevnetConfigTui } from '../tui/devnet-config-tui'; + +export interface DevnetConfigOptions { + set?: string[]; +} + +export interface ParsedSetItem { + key: string; + value: string; +} + +export function parseSetItem(item: string): ParsedSetItem { + const separator = item.indexOf('='); + if (separator <= 0 || separator === item.length - 1) { + throw new Error(`Invalid --set item '${item}'. Use key=value format.`); + } + + const key = item.slice(0, separator).trim(); + const value = item.slice(separator + 1).trim(); + if (!key || !value) { + throw new Error(`Invalid --set item '${item}'. Key and value must not be empty.`); + } + + return { key, value }; +} + +export function applySetItems(editor: ReturnType, items: string[]): ParsedSetItem[] { + const parsedItems = items.map(parseSetItem); + for (const parsedItem of parsedItems) { + editor.setFieldValue(parsedItem.key, parsedItem.value); + } + editor.save(); + return parsedItems; +} + +export async function devnetConfig(options: DevnetConfigOptions = {}) { + const settings = readSettings(); + const configPath = settings.devnet.configPath; + + try { + const editor = createDevnetConfigEditor(configPath); + + if (options.set && options.set.length > 0) { + const parsedItems = applySetItems(editor, options.set); + logger.success(`Devnet config updated at: ${configPath}`); + logger.info( + `Applied ${parsedItems.length} setting(s): ${parsedItems.map((item) => `${item.key}=${item.value}`).join(', ')}`, + ); + logger.info('Restart devnet to apply changes: offckb clean -d && offckb node'); + return; + } + + if (!process.stdin.isTTY || !process.stdout.isTTY) { + logger.error('Interactive devnet config editor requires a TTY terminal.'); + logger.info('Use non-interactive mode instead, e.g.:'); + logger.info(' offckb devnet config --set ckb.logger.filter=info'); + logger.info(' offckb devnet config --set miner.client.poll_interval=1500'); + process.exitCode = 1; + return; + } + + const isSaved = await runDevnetConfigTui(editor, configPath); + + if (isSaved) { + logger.success(`Devnet config updated at: ${configPath}`); + logger.info('Restart devnet to apply changes: offckb clean -d && offckb node'); + return; + } + + logger.info('No changes saved.'); + } catch (error) { + logger.error((error as Error).message); + logger.info('Tip: run `offckb node` once to initialize devnet config files first.'); + process.exitCode = 1; + } +} diff --git a/src/devnet/config-editor.ts b/src/devnet/config-editor.ts new file mode 100644 index 0000000..5c280dc --- /dev/null +++ b/src/devnet/config-editor.ts @@ -0,0 +1,695 @@ +import fs from 'fs'; +import path from 'path'; +import toml, { JsonMap } from '@iarna/toml'; + +type FieldType = 'string' | 'number' | 'boolean'; + +type EditableFieldValue = string | number | boolean; + +export type TomlPrimitive = string | number | boolean; +export type TomlValue = TomlPrimitive | TomlObject | TomlValue[]; +export interface TomlObject { + [key: string]: TomlValue; +} + +interface EditableFieldDefinition { + id: string; + file: 'ckb' | 'miner'; + label: string; + description: string; + type: FieldType; + path: Array; +} + +export interface EditableField extends EditableFieldDefinition { + value: EditableFieldValue; +} + +export interface TomlDocument { + id: 'ckb' | 'miner'; + title: string; + filePath: string; + data: TomlObject; +} + +export interface TomlEntry { + documentId: 'ckb' | 'miner'; + path: string[]; + pathText: string; + type: 'object' | 'array' | 'string' | 'number' | 'boolean'; + valuePreview: string; + editable: boolean; +} + +const editableFieldDefinitions: EditableFieldDefinition[] = [ + { + id: 'ckb.logger.filter', + file: 'ckb', + label: 'Logger filter', + description: 'CKB log filter string', + type: 'string', + path: ['logger', 'filter'], + }, + { + id: 'ckb.logger.color', + file: 'ckb', + label: 'Logger color output', + description: 'Enable colorful logs', + type: 'boolean', + path: ['logger', 'color'], + }, + { + id: 'ckb.logger.log_to_file', + file: 'ckb', + label: 'Logger output to file', + description: 'Write logs to file', + type: 'boolean', + path: ['logger', 'log_to_file'], + }, + { + id: 'ckb.logger.log_to_stdout', + file: 'ckb', + label: 'Logger output to stdout', + description: 'Write logs to stdout', + type: 'boolean', + path: ['logger', 'log_to_stdout'], + }, + { + id: 'ckb.rpc.listen_address', + file: 'ckb', + label: 'RPC listen address', + description: 'Host:port for CKB RPC', + type: 'string', + path: ['rpc', 'listen_address'], + }, + { + id: 'ckb.rpc.max_request_body_size', + file: 'ckb', + label: 'RPC max request body size', + description: 'Maximum request body size in bytes', + type: 'number', + path: ['rpc', 'max_request_body_size'], + }, + { + id: 'ckb.rpc.enable_deprecated_rpc', + file: 'ckb', + label: 'Enable deprecated RPC', + description: 'Allow deprecated CKB RPC methods', + type: 'boolean', + path: ['rpc', 'enable_deprecated_rpc'], + }, + { + id: 'miner.client.rpc_url', + file: 'miner', + label: 'Miner RPC URL', + description: 'CKB node URL used by miner', + type: 'string', + path: ['miner', 'client', 'rpc_url'], + }, + { + id: 'miner.client.block_on_submit', + file: 'miner', + label: 'Miner block on submit', + description: 'Wait for submit result before next request', + type: 'boolean', + path: ['miner', 'client', 'block_on_submit'], + }, + { + id: 'miner.client.poll_interval', + file: 'miner', + label: 'Miner poll interval', + description: 'Block template polling interval in milliseconds', + type: 'number', + path: ['miner', 'client', 'poll_interval'], + }, +]; + +const safeFieldDefinitionMap = new Map( + editableFieldDefinitions.map((definition) => [ + `${definition.file}:${definition.path.map((item) => String(item)).join('.')}`, + definition, + ]), +); + +function getByPath(target: Record, keyPath: Array): unknown { + let current: unknown = target; + for (const key of keyPath) { + if (current == null || typeof current !== 'object') { + return undefined; + } + current = (current as Record)[String(key)]; + } + return current; +} + +function setByPath(target: Record, keyPath: Array, value: unknown): void { + if (keyPath.length === 0) { + return; + } + + let current: Record = target; + for (let i = 0; i < keyPath.length - 1; i++) { + const part = String(keyPath[i]); + const next = current[part]; + if (next == null || typeof next !== 'object') { + current[part] = {}; + } + current = current[part] as Record; + } + + current[String(keyPath[keyPath.length - 1])] = value; +} + +function isPlainObject(value: unknown): value is Record { + return value != null && typeof value === 'object' && !Array.isArray(value); +} + +function isTomlPrimitive(value: unknown): value is TomlPrimitive { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'; +} + +function getTypeOfTomlValue(value: unknown): TomlEntry['type'] { + if (Array.isArray(value)) { + return 'array'; + } + if (isPlainObject(value)) { + return 'object'; + } + if (typeof value === 'string') { + return 'string'; + } + if (typeof value === 'number') { + return 'number'; + } + return 'boolean'; +} + +function valuePreview(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.length}]`; + } + if (isPlainObject(value)) { + return `[${Object.keys(value).length} keys]`; + } + if (typeof value === 'string') { + if (value.length > 80) { + return `${value.slice(0, 80)}...`; + } + return value; + } + return String(value); +} + +function validateHostPort(value: string): boolean { + const trimmed = value.trim(); + const separator = trimmed.lastIndexOf(':'); + if (separator <= 0 || separator === trimmed.length - 1) { + return false; + } + + const port = Number(trimmed.slice(separator + 1)); + return Number.isInteger(port) && port > 0 && port <= 65535; +} + +function validateHttpUrl(value: string): boolean { + try { + const parsed = new URL(value); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch { + return false; + } +} + +function normalizeBooleanInput(value: string): boolean | null { + const normalized = value.trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) { + return true; + } + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) { + return false; + } + return null; +} + +function parseValueByExistingType(rawInput: string, existingValue: unknown): TomlPrimitive { + if (typeof existingValue === 'boolean') { + const parsedBoolean = normalizeBooleanInput(rawInput); + if (parsedBoolean == null) { + throw new Error('Boolean value must be one of: true/false/yes/no/1/0.'); + } + return parsedBoolean; + } + + if (typeof existingValue === 'number') { + const parsedNumber = Number(rawInput.trim()); + if (!Number.isFinite(parsedNumber)) { + throw new Error('Number value must be finite.'); + } + return parsedNumber; + } + + const trimmed = rawInput.trim(); + if (!trimmed) { + throw new Error('Value cannot be empty.'); + } + return trimmed; +} + +function parseInputAsTomlPrimitive(rawInput: string): TomlPrimitive { + const trimmed = rawInput.trim(); + if (!trimmed) { + throw new Error('Value cannot be empty.'); + } + + const parsedBoolean = normalizeBooleanInput(trimmed); + if (parsedBoolean != null) { + return parsedBoolean; + } + + const parsedNumber = Number(trimmed); + if (!Number.isNaN(parsedNumber) && Number.isFinite(parsedNumber)) { + return parsedNumber; + } + + return trimmed; +} + +function readTomlFile(filePath: string): Record { + const text = fs.readFileSync(filePath, 'utf8'); + return toml.parse(text) as unknown as Record; +} + +function writeTomlFileAtomic(filePath: string, data: Record) { + const tempFilePath = `${filePath}.tmp`; + const text = toml.stringify(data as unknown as JsonMap); + fs.writeFileSync(tempFilePath, text, 'utf8'); + fs.renameSync(tempFilePath, filePath); +} + +function requirePath( + target: Record, + pathParts: string[], + guard: (value: unknown) => value is T, + message: string, +): T { + const value = getByPath(target, pathParts); + if (!guard(value)) { + throw new Error(message); + } + return value; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string'); +} + +export class DevnetConfigEditor { + readonly configPath: string; + readonly ckbTomlPath: string; + readonly minerTomlPath: string; + + private ckbConfig: Record; + private minerConfig: Record; + private values: Record; + private documents: Record<'ckb' | 'miner', TomlDocument>; + + constructor(configPath: string, ckbConfig: Record, minerConfig: Record) { + this.configPath = configPath; + this.ckbTomlPath = path.join(configPath, 'ckb.toml'); + this.minerTomlPath = path.join(configPath, 'ckb-miner.toml'); + this.ckbConfig = ckbConfig; + this.minerConfig = minerConfig; + + this.values = {}; + for (const definition of editableFieldDefinitions) { + const source = definition.file === 'ckb' ? this.ckbConfig : this.minerConfig; + const value = getByPath(source, definition.path); + if (value == null) { + throw new Error(`Unsupported config layout: missing field '${definition.id}' in devnet config files.`); + } + + if (definition.type === 'string' && typeof value !== 'string') { + throw new Error(`Unexpected type for '${definition.id}', expected string.`); + } + if (definition.type === 'number' && typeof value !== 'number') { + throw new Error(`Unexpected type for '${definition.id}', expected number.`); + } + if (definition.type === 'boolean' && typeof value !== 'boolean') { + throw new Error(`Unexpected type for '${definition.id}', expected boolean.`); + } + + this.values[definition.id] = value as EditableFieldValue; + } + + this.documents = { + ckb: { + id: 'ckb', + title: 'ckb.toml', + filePath: this.ckbTomlPath, + data: this.ckbConfig as TomlObject, + }, + miner: { + id: 'miner', + title: 'ckb-miner.toml', + filePath: this.minerTomlPath, + data: this.minerConfig as TomlObject, + }, + }; + } + + getDocuments(): TomlDocument[] { + return [this.documents.ckb, this.documents.miner]; + } + + getDocument(documentId: 'ckb' | 'miner'): TomlDocument { + return this.documents[documentId]; + } + + getEntriesForDocument(documentId: 'ckb' | 'miner'): TomlEntry[] { + const entries: TomlEntry[] = []; + const document = this.getDocument(documentId); + + const walk = (value: unknown, currentPath: string[]) => { + if (currentPath.length > 0) { + const entryType = getTypeOfTomlValue(value); + entries.push({ + documentId, + path: currentPath, + pathText: currentPath.join('.'), + type: entryType, + valuePreview: entryType === 'object' ? '' : valuePreview(value), + editable: entryType === 'string' || entryType === 'number' || entryType === 'boolean', + }); + } + + if (Array.isArray(value)) { + value.forEach((item, index) => walk(item, [...currentPath, String(index)])); + return; + } + + if (isPlainObject(value)) { + for (const key of Object.keys(value)) { + walk(value[key], [...currentPath, key]); + } + } + }; + + walk(document.data, []); + return entries; + } + + getEntryValue(documentId: 'ckb' | 'miner', pathParts: string[]): unknown { + return getByPath(this.getDocument(documentId).data as Record, pathParts); + } + + getFields(): EditableField[] { + return editableFieldDefinitions.map((definition) => ({ + ...definition, + value: this.values[definition.id], + })); + } + + getField(fieldId: string): EditableField { + const definition = editableFieldDefinitions.find((item) => item.id === fieldId); + if (definition == null) { + throw new Error(`Unknown field '${fieldId}'.`); + } + + return { + ...definition, + value: this.values[definition.id], + }; + } + + setFieldValue(fieldId: string, rawInput: string): EditableFieldValue { + const definition = editableFieldDefinitions.find((item) => item.id === fieldId); + if (definition == null) { + throw new Error(`Unknown field '${fieldId}'.`); + } + + const trimmed = rawInput.trim(); + if (definition.type === 'string') { + if (!trimmed) { + throw new Error('Value cannot be empty.'); + } + + if (definition.id === 'ckb.rpc.listen_address' && !validateHostPort(trimmed)) { + throw new Error('RPC listen address must be in host:port format.'); + } + + if (definition.id === 'miner.client.rpc_url' && !validateHttpUrl(trimmed)) { + throw new Error('Miner RPC URL must be a valid HTTP/HTTPS URL.'); + } + + this.values[fieldId] = trimmed; + setByPath( + this.getDocument(definition.file).data as Record, + definition.path, + this.values[fieldId], + ); + return this.values[fieldId]; + } + + if (definition.type === 'number') { + const parsed = Number(trimmed); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error('Value must be a positive integer.'); + } + + this.values[fieldId] = parsed; + setByPath( + this.getDocument(definition.file).data as Record, + definition.path, + this.values[fieldId], + ); + return this.values[fieldId]; + } + + const parsedBoolean = normalizeBooleanInput(trimmed); + if (parsedBoolean == null) { + throw new Error('Boolean value must be one of: true/false/yes/no/1/0.'); + } + + this.values[fieldId] = parsedBoolean; + setByPath(this.getDocument(definition.file).data as Record, definition.path, this.values[fieldId]); + return this.values[fieldId]; + } + + setDocumentValue(documentId: 'ckb' | 'miner', pathParts: string[], rawInput: string): TomlPrimitive { + if (pathParts.length === 0) { + throw new Error('Cannot edit the root node.'); + } + + const fileKey = `${documentId}:${pathParts.join('.')}`; + const safeDefinition = safeFieldDefinitionMap.get(fileKey); + if (safeDefinition != null) { + return this.setFieldValue(safeDefinition.id, rawInput); + } + + const document = this.getDocument(documentId); + const currentValue = getByPath(document.data as Record, pathParts); + if (!isTomlPrimitive(currentValue)) { + throw new Error('Only primitive values can be edited directly.'); + } + + const parsedValue = parseValueByExistingType(rawInput, currentValue); + setByPath(document.data as Record, pathParts, parsedValue); + return parsedValue; + } + + addObjectEntry(documentId: 'ckb' | 'miner', pathParts: string[], key: string, rawValue: string): TomlPrimitive { + const target = getByPath(this.getDocument(documentId).data as Record, pathParts); + if (!isPlainObject(target)) { + throw new Error('Target path is not an object.'); + } + + const normalizedKey = key.trim(); + if (!normalizedKey) { + throw new Error('Key cannot be empty.'); + } + if (normalizedKey in target) { + throw new Error(`Key '${normalizedKey}' already exists.`); + } + + const parsedValue = parseInputAsTomlPrimitive(rawValue); + target[normalizedKey] = parsedValue; + return parsedValue; + } + + appendArrayEntry(documentId: 'ckb' | 'miner', pathParts: string[], rawValue: string): TomlPrimitive { + const target = getByPath(this.getDocument(documentId).data as Record, pathParts); + if (!Array.isArray(target)) { + throw new Error('Target path is not an array.'); + } + + const parsedValue = parseInputAsTomlPrimitive(rawValue); + target.push(parsedValue); + return parsedValue; + } + + insertArrayEntry(documentId: 'ckb' | 'miner', pathParts: string[], index: number, rawValue: string): TomlPrimitive { + const target = getByPath(this.getDocument(documentId).data as Record, pathParts); + if (!Array.isArray(target)) { + throw new Error('Target path is not an array.'); + } + + if (!Number.isInteger(index) || index < 0 || index > target.length) { + throw new Error(`Insert index must be between 0 and ${target.length}.`); + } + + const parsedValue = parseInputAsTomlPrimitive(rawValue); + target.splice(index, 0, parsedValue); + return parsedValue; + } + + moveArrayEntry(documentId: 'ckb' | 'miner', pathParts: string[], fromIndex: number, toIndex: number): void { + const target = getByPath(this.getDocument(documentId).data as Record, pathParts); + if (!Array.isArray(target)) { + throw new Error('Target path is not an array.'); + } + + if (!Number.isInteger(fromIndex) || fromIndex < 0 || fromIndex >= target.length) { + throw new Error(`Source index must be between 0 and ${Math.max(0, target.length - 1)}.`); + } + if (!Number.isInteger(toIndex) || toIndex < 0 || toIndex >= target.length) { + throw new Error(`Target index must be between 0 and ${Math.max(0, target.length - 1)}.`); + } + + if (fromIndex === toIndex) { + return; + } + + const [item] = target.splice(fromIndex, 1); + target.splice(toIndex, 0, item); + } + + setArrayValues(documentId: 'ckb' | 'miner', pathParts: string[], values: string[]): void { + const target = getByPath(this.getDocument(documentId).data as Record, pathParts); + if (!Array.isArray(target)) { + throw new Error('Target path is not an array.'); + } + target.splice(0, target.length, ...values); + } + + deleteDocumentPath(documentId: 'ckb' | 'miner', pathParts: string[]): void { + if (pathParts.length === 0) { + throw new Error('Cannot delete the root node.'); + } + + const parentPath = pathParts.slice(0, -1); + const key = pathParts[pathParts.length - 1]; + const parent = getByPath(this.getDocument(documentId).data as Record, parentPath); + + if (Array.isArray(parent)) { + const index = Number(key); + if (!Number.isInteger(index) || index < 0 || index >= parent.length) { + throw new Error('Invalid array index for deletion.'); + } + parent.splice(index, 1); + return; + } + + if (isPlainObject(parent)) { + if (!(key in parent)) { + throw new Error(`Key '${key}' does not exist.`); + } + delete parent[key]; + return; + } + + throw new Error('Target path cannot be deleted.'); + } + + toggleBooleanField(fieldId: string): EditableFieldValue { + const field = this.getField(fieldId); + if (field.type !== 'boolean') { + throw new Error(`Field '${fieldId}' is not boolean.`); + } + + const nextValue = !field.value; + this.values[fieldId] = nextValue; + setByPath(this.getDocument(field.file).data as Record, field.path, nextValue); + return nextValue; + } + + private validateBeforeSave(): void { + const ckb = this.documents.ckb.data as unknown as Record; + const miner = this.documents.miner.data as unknown as Record; + + const listenAddress = requirePath( + ckb, + ['rpc', 'listen_address'], + isNonEmptyString, + 'Invalid config: rpc.listen_address must be a non-empty string.', + ); + if (!validateHostPort(listenAddress)) { + throw new Error('Invalid config: rpc.listen_address must be in host:port format.'); + } + + const rpcModules = requirePath( + ckb, + ['rpc', 'modules'], + isStringArray, + 'Invalid config: rpc.modules must be an array of strings.', + ); + if (rpcModules.length === 0) { + throw new Error('Invalid config: rpc.modules must include at least one module.'); + } + if (rpcModules.some((moduleName) => moduleName.trim().length === 0)) { + throw new Error('Invalid config: rpc.modules must not contain empty module names.'); + } + + const supportProtocols = requirePath( + ckb, + ['network', 'support_protocols'], + isStringArray, + 'Invalid config: network.support_protocols must be an array of strings.', + ); + if (!supportProtocols.includes('Sync') || !supportProtocols.includes('Identify')) { + throw new Error('Invalid config: network.support_protocols must include both Sync and Identify.'); + } + if (supportProtocols.some((protocol) => protocol.trim().length === 0)) { + throw new Error('Invalid config: network.support_protocols must not contain empty protocol names.'); + } + + const minerRpcUrl = requirePath( + miner, + ['miner', 'client', 'rpc_url'], + isNonEmptyString, + 'Invalid config: miner.client.rpc_url must be a non-empty string.', + ); + if (!validateHttpUrl(minerRpcUrl)) { + throw new Error('Invalid config: miner.client.rpc_url must be a valid HTTP/HTTPS URL.'); + } + } + + save(): void { + this.validateBeforeSave(); + writeTomlFileAtomic(this.ckbTomlPath, this.documents.ckb.data as unknown as Record); + writeTomlFileAtomic(this.minerTomlPath, this.documents.miner.data as unknown as Record); + } +} + +export function createDevnetConfigEditor(configPath: string): DevnetConfigEditor { + const ckbTomlPath = path.join(configPath, 'ckb.toml'); + const minerTomlPath = path.join(configPath, 'ckb-miner.toml'); + + if (!fs.existsSync(configPath)) { + throw new Error(`Devnet config path does not exist: ${configPath}`); + } + if (!fs.existsSync(ckbTomlPath)) { + throw new Error(`Missing file: ${ckbTomlPath}`); + } + if (!fs.existsSync(minerTomlPath)) { + throw new Error(`Missing file: ${minerTomlPath}`); + } + + const ckbConfig = readTomlFile(ckbTomlPath); + const minerConfig = readTomlFile(minerTomlPath); + + return new DevnetConfigEditor(configPath, ckbConfig, minerConfig); +} diff --git a/src/tui/actions.ts b/src/tui/actions.ts new file mode 100644 index 0000000..073aa91 --- /dev/null +++ b/src/tui/actions.ts @@ -0,0 +1,489 @@ +import { TomlEntry } from '../devnet/config-editor'; +import { getFixedArraySpecFromEntryPath, FixedArraySpec } from './devnet-config-metadata'; +import { waitForInput, waitForConfirm, waitForFixedArraySelection, waitForArrayValue } from './dialogs'; +import { TuiState, TuiWidgets } from './tui-state'; + +// --------------------------------------------------------------------------- +// Pure helpers +// --------------------------------------------------------------------------- + +export function parseNonNegativeInteger(value: string, fieldName: string): number { + const parsed = Number(value.trim()); + if (!Number.isInteger(parsed) || parsed < 0) { + throw new Error(`${fieldName} must be a non-negative integer.`); + } + return parsed; +} + +export function resolveArrayTarget( + selectedEntry: TomlEntry, +): { arrayPath: string[]; suggestedIndex: number | null } | null { + if (selectedEntry.type === 'array') { + return { arrayPath: selectedEntry.path, suggestedIndex: null }; + } + + const lastPart = selectedEntry.path[selectedEntry.path.length - 1]; + const parsedIndex = Number(lastPart); + if (Number.isInteger(parsedIndex) && parsedIndex >= 0) { + return { arrayPath: selectedEntry.path.slice(0, -1), suggestedIndex: parsedIndex }; + } + + return null; +} + +export function resolveFixedArrayTarget( + selectedEntry: TomlEntry, +): { arrayPath: string[]; spec: FixedArraySpec } | null { + const spec = getFixedArraySpecFromEntryPath(selectedEntry.path); + if (spec == null) return null; + + if (selectedEntry.type === 'array') { + return { arrayPath: selectedEntry.path, spec }; + } + + const lastPart = selectedEntry.path[selectedEntry.path.length - 1]; + if (/^\d+$/.test(lastPart)) { + return { arrayPath: selectedEntry.path.slice(0, -1), spec }; + } + + return { arrayPath: selectedEntry.path, spec }; +} + +// --------------------------------------------------------------------------- +// Shared sub-action: edit a fixed-array via multi-select dialog +// --------------------------------------------------------------------------- + +export async function editFixedArraySelection( + state: TuiState, + screen: import('blessed').Widgets.Screen, + documentId: 'ckb' | 'miner', + arrayPath: string[], + spec: FixedArraySpec, + refreshUi: () => void, +): Promise { + const rawArrayValue = state.editor.getEntryValue(documentId, arrayPath); + if (!Array.isArray(rawArrayValue)) { + state.statusMessage = `Path ${arrayPath.join('.')} is not an array.`; + refreshUi(); + return; + } + + const currentValues = rawArrayValue.map((item) => String(item)); + const selectedValues = await waitForFixedArraySelection(screen, `Edit ${spec.label}`, spec, currentValues); + if (selectedValues == null) { + state.statusMessage = 'Edit canceled.'; + refreshUi(); + return; + } + + const nextValues = spec.unique ? Array.from(new Set(selectedValues)) : selectedValues; + state.editor.setArrayValues(documentId, arrayPath, nextValues); + state.hasUnsavedChanges = true; + state.statusMessage = `Updated ${arrayPath.join('.')} (${nextValues.length} selected).`; + refreshUi(); +} + +// --------------------------------------------------------------------------- +// Action context: common args passed to every action +// --------------------------------------------------------------------------- + +export interface ActionContext { + state: TuiState; + widgets: TuiWidgets; + refreshUi: () => void; +} + +function currentDocument(ctx: ActionContext) { + return ctx.state.documents[ctx.state.selectedDocumentIndex]; +} + +function currentEntry(ctx: ActionContext): TomlEntry | null { + if (ctx.state.visibleEntries.length === 0) return null; + return ctx.state.visibleEntries[ctx.state.selectedEntryIndex] ?? null; +} + +// --------------------------------------------------------------------------- +// Actions +// --------------------------------------------------------------------------- + +export async function editCurrentEntry(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + + if (state.focusPane !== 'entries') { + state.focusPane = 'entries'; + refreshUi(); + return; + } + + const doc = currentDocument(ctx); + const entry = currentEntry(ctx); + if (entry == null) return; + + const fixedTarget = resolveFixedArrayTarget(entry); + if (fixedTarget != null) { + await editFixedArraySelection(state, widgets.screen, doc.id, fixedTarget.arrayPath, fixedTarget.spec, refreshUi); + return; + } + + if (!entry.editable) { + state.statusMessage = `Path ${entry.pathText} is not primitive-editable yet.`; + refreshUi(); + return; + } + + const value = state.editor.getEntryValue(entry.documentId, entry.path); + const valueText = value == null ? '' : String(value); + const answer = await waitForArrayValue(widgets.screen, null, 'Edit Value', entry.pathText, valueText); + if (answer == null) { + state.statusMessage = 'Edit canceled.'; + refreshUi(); + return; + } + + try { + state.editor.setDocumentValue(entry.documentId, entry.path, answer); + state.hasUnsavedChanges = true; + state.statusMessage = `Updated ${entry.pathText}.`; + } catch (error) { + state.statusMessage = `Validation error: ${(error as Error).message}`; + } + + refreshUi(); +} + +export async function searchEntries(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + + const answer = await waitForInput( + widgets.screen, + 'Search', + 'Path/type/value filter (empty clears):', + state.searchTerm, + ); + if (answer == null) { + state.statusMessage = 'Search canceled.'; + refreshUi(); + return; + } + + state.searchTerm = answer.trim(); + state.selectedEntryIndex = 0; + state.statusMessage = state.searchTerm ? `Filter applied: ${state.searchTerm}` : 'Search filter cleared.'; + refreshUi(); +} + +export function jumpSearchMatch(ctx: ActionContext, direction: 'next' | 'prev'): void { + const { state, refreshUi } = ctx; + + if (state.visibleEntries.length === 0) { + state.statusMessage = state.searchTerm ? 'No search matches to jump.' : 'Set search filter first with /.'; + refreshUi(); + return; + } + + if (direction === 'next') { + state.selectedEntryIndex = (state.selectedEntryIndex + 1) % state.visibleEntries.length; + state.statusMessage = `Jumped to next match (${state.selectedEntryIndex + 1}/${state.visibleEntries.length}).`; + } else { + state.selectedEntryIndex = + (state.selectedEntryIndex - 1 + state.visibleEntries.length) % state.visibleEntries.length; + state.statusMessage = `Jumped to previous match (${state.selectedEntryIndex + 1}/${state.visibleEntries.length}).`; + } + + refreshUi(); +} + +export async function addEntry(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + const doc = currentDocument(ctx); + + const targetEntry = + state.visibleEntries.length > 0 + ? state.visibleEntries[Math.min(state.selectedEntryIndex, state.visibleEntries.length - 1)] + : null; + + const targetPath = targetEntry?.path ?? []; + const targetValue = state.editor.getEntryValue(doc.id, targetPath); + + if (targetEntry == null && !Array.isArray(targetValue) && (targetValue == null || typeof targetValue !== 'object')) { + state.statusMessage = 'No target object/array selected for add.'; + refreshUi(); + return; + } + + if ( + targetEntry?.type === 'object' || + (targetEntry == null && targetValue != null && typeof targetValue === 'object') + ) { + const keyAnswer = await waitForInput(widgets.screen, 'Add Object Key', 'New key name:', ''); + if (keyAnswer == null) { + state.statusMessage = 'Add canceled.'; + refreshUi(); + return; + } + + const valueAnswer = await waitForInput( + widgets.screen, + 'Add Object Key', + `Value for ${keyAnswer.trim()} (auto parse bool/number):`, + '', + ); + if (valueAnswer == null) { + state.statusMessage = 'Add canceled.'; + refreshUi(); + return; + } + + try { + state.editor.addObjectEntry(doc.id, targetPath, keyAnswer, valueAnswer); + state.hasUnsavedChanges = true; + state.statusMessage = `Added key '${keyAnswer.trim()}' under ${targetPath.join('.') || ''}.`; + } catch (error) { + state.statusMessage = `Add failed: ${(error as Error).message}`; + } + refreshUi(); + return; + } + + if (targetEntry?.type === 'array') { + const fixedTarget = resolveFixedArrayTarget(targetEntry); + if (fixedTarget != null) { + await editFixedArraySelection(state, widgets.screen, doc.id, fixedTarget.arrayPath, fixedTarget.spec, refreshUi); + return; + } + + const valueAnswer = await waitForArrayValue( + widgets.screen, + null, + 'Append Array Item', + `Append value to ${targetEntry.pathText}:`, + '', + ); + if (valueAnswer == null) { + state.statusMessage = 'Append canceled.'; + refreshUi(); + return; + } + + try { + state.editor.appendArrayEntry(doc.id, targetEntry.path, valueAnswer); + state.hasUnsavedChanges = true; + state.statusMessage = `Appended value to ${targetEntry.pathText}.`; + } catch (error) { + state.statusMessage = `Append failed: ${(error as Error).message}`; + } + refreshUi(); + return; + } + + state.statusMessage = 'Select an object or array node to add items.'; + refreshUi(); +} + +export async function insertArrayEntry(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + + const entry = currentEntry(ctx); + if (entry == null) { + state.statusMessage = 'No selected entry for insert.'; + refreshUi(); + return; + } + + const doc = currentDocument(ctx); + const target = resolveArrayTarget(entry); + if (target == null) { + state.statusMessage = 'Select an array or array item to insert.'; + refreshUi(); + return; + } + + const fixedSpec = getFixedArraySpecFromEntryPath(target.arrayPath); + if (fixedSpec != null) { + await editFixedArraySelection(state, widgets.screen, doc.id, target.arrayPath, fixedSpec, refreshUi); + return; + } + + const arrayValue = state.editor.getEntryValue(doc.id, target.arrayPath); + const arrayLength = Array.isArray(arrayValue) ? arrayValue.length : 0; + const indexAnswer = await waitForInput( + widgets.screen, + 'Insert Array Item', + `Insert index (0-${arrayLength}${target.suggestedIndex != null ? `, default ${target.suggestedIndex}` : ''}):`, + target.suggestedIndex != null ? String(target.suggestedIndex) : String(arrayLength), + ); + if (indexAnswer == null) { + state.statusMessage = 'Insert canceled.'; + refreshUi(); + return; + } + + const valueAnswer = await waitForArrayValue( + widgets.screen, + null, + 'Insert Array Item', + `Value to insert at ${target.arrayPath.join('.')} (auto parse bool/number):`, + '', + ); + if (valueAnswer == null) { + state.statusMessage = 'Insert canceled.'; + refreshUi(); + return; + } + + try { + const indexInput = indexAnswer.trim(); + const insertIndex = + indexInput === '' && target.suggestedIndex != null + ? target.suggestedIndex + : parseNonNegativeInteger(indexAnswer, 'Insert index'); + + state.editor.insertArrayEntry(doc.id, target.arrayPath, insertIndex, valueAnswer); + state.hasUnsavedChanges = true; + state.statusMessage = `Inserted array item at ${target.arrayPath.join('.')}[${insertIndex}].`; + } catch (error) { + state.statusMessage = `Insert failed: ${(error as Error).message}`; + } + + refreshUi(); +} + +export async function moveArrayEntry(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + + const entry = currentEntry(ctx); + if (entry == null) { + state.statusMessage = 'No selected entry for move.'; + refreshUi(); + return; + } + + const doc = currentDocument(ctx); + const target = resolveArrayTarget(entry); + if (target == null) { + state.statusMessage = 'Select an array or array item to move.'; + refreshUi(); + return; + } + + const fixedSpec = getFixedArraySpecFromEntryPath(target.arrayPath); + if (fixedSpec != null) { + await editFixedArraySelection(state, widgets.screen, doc.id, target.arrayPath, fixedSpec, refreshUi); + return; + } + + const arrayValue = state.editor.getEntryValue(doc.id, target.arrayPath); + const arrayLength = Array.isArray(arrayValue) ? arrayValue.length : 0; + + const fromAnswer = await waitForInput( + widgets.screen, + 'Move Array Item', + `Move from index (0-${Math.max(0, arrayLength - 1)}${target.suggestedIndex != null ? `, default ${target.suggestedIndex}` : ''}):`, + target.suggestedIndex != null ? String(target.suggestedIndex) : '0', + ); + if (fromAnswer == null) { + state.statusMessage = 'Move canceled.'; + refreshUi(); + return; + } + + const toAnswer = await waitForInput( + widgets.screen, + 'Move Array Item', + `Move to index (0-${Math.max(0, arrayLength - 1)}):`, + '0', + ); + if (toAnswer == null) { + state.statusMessage = 'Move canceled.'; + refreshUi(); + return; + } + + try { + const fromInput = fromAnswer.trim(); + const fromIndex = + fromInput === '' && target.suggestedIndex != null + ? target.suggestedIndex + : parseNonNegativeInteger(fromAnswer, 'Source index'); + const toIndex = parseNonNegativeInteger(toAnswer, 'Target index'); + + state.editor.moveArrayEntry(doc.id, target.arrayPath, fromIndex, toIndex); + state.hasUnsavedChanges = true; + state.statusMessage = `Moved item in ${target.arrayPath.join('.')} from ${fromIndex} to ${toIndex}.`; + } catch (error) { + state.statusMessage = `Move failed: ${(error as Error).message}`; + } + + refreshUi(); +} + +export async function deleteEntry(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + const doc = currentDocument(ctx); + + const entry = currentEntry(ctx); + if (entry == null) { + state.statusMessage = 'No selected entry to delete.'; + refreshUi(); + return; + } + + const fixedTarget = resolveFixedArrayTarget(entry); + if (fixedTarget != null) { + await editFixedArraySelection(state, widgets.screen, doc.id, fixedTarget.arrayPath, fixedTarget.spec, refreshUi); + return; + } + + const confirmed = await waitForConfirm(widgets.screen, 'Delete Path', `Delete ${entry.pathText}?`); + if (!confirmed) { + state.statusMessage = 'Delete canceled.'; + refreshUi(); + return; + } + + try { + state.editor.deleteDocumentPath(doc.id, entry.path); + state.hasUnsavedChanges = true; + state.selectedEntryIndex = Math.max(0, state.selectedEntryIndex - 1); + state.statusMessage = `Deleted ${entry.pathText}.`; + } catch (error) { + state.statusMessage = `Delete failed: ${(error as Error).message}`; + } + refreshUi(); +} + +export async function quitFlow(ctx: ActionContext): Promise { + const { state, widgets, refreshUi } = ctx; + + if (!state.hasUnsavedChanges) { + widgets.screen.destroy(); + return; + } + + const shouldDiscard = await waitForConfirm( + widgets.screen, + 'Unsaved Changes', + 'You have unsaved changes. Press S to save and exit, or discard changes and exit.', + { + confirmLabel: 'Discard & Exit', + cancelLabel: 'Keep Editing', + defaultFocus: 'cancel', + }, + ); + if (shouldDiscard) { + widgets.screen.destroy(); + return; + } + + state.statusMessage = 'Continue editing.'; + refreshUi(); +} + +export function saveAndExit(ctx: ActionContext): void { + const { state, widgets } = ctx; + state.editor.save(); + state.hasUnsavedChanges = false; + state.didSave = true; + state.statusMessage = 'Saved.'; + widgets.screen.destroy(); +} diff --git a/src/tui/blessed-helpers.ts b/src/tui/blessed-helpers.ts new file mode 100644 index 0000000..2fac76b --- /dev/null +++ b/src/tui/blessed-helpers.ts @@ -0,0 +1,9 @@ +import { Widgets } from 'blessed'; + +/** + * Safely read the `selected` index from a blessed list element. + * Blessed exposes `.selected` at runtime but the type declarations omit it. + */ +export function getListSelected(list: Widgets.ListElement): number { + return (list as unknown as { selected?: number }).selected ?? 0; +} diff --git a/src/tui/devnet-config-metadata.ts b/src/tui/devnet-config-metadata.ts new file mode 100644 index 0000000..5fa300e --- /dev/null +++ b/src/tui/devnet-config-metadata.ts @@ -0,0 +1,237 @@ +export interface ConfigDoc { + summary: string; + source: string; +} + +interface ConfigDocRule { + pathPattern: string; + doc: ConfigDoc; +} + +export interface FixedArraySpec { + pathPattern: string; + label: string; + options: string[]; + unique: boolean; + allowCustom: boolean; + source: string; +} + +const CONFIG_DOCS: ConfigDocRule[] = [ + { + pathPattern: 'logger.filter', + doc: { + summary: 'Rust log filter directive list used by CKB logger.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'rpc.listen_address', + doc: { + summary: 'Address for JSON-RPC server binding (host:port).', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'rpc.max_request_body_size', + doc: { + summary: 'Maximum HTTP RPC request body size in bytes.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'rpc.modules', + doc: { + summary: 'Enabled RPC modules; known defaults plus custom module names if supported.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'rpc.modules.#', + doc: { + summary: 'One enabled RPC module entry.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.listen_addresses', + doc: { + summary: 'Node P2P listen multiaddr list.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.listen_addresses.#', + doc: { + summary: 'One P2P listen multiaddr entry.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.bootnodes', + doc: { + summary: 'Seed peers used for bootstrap discovery.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.bootnodes.#', + doc: { + summary: 'One bootnode multiaddr entry.', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.max_peers', + doc: { + summary: 'Maximum connected peers (inbound + outbound).', + source: 'CKB configure docs', + }, + }, + { + pathPattern: 'network.support_protocols', + doc: { + summary: 'Enabled CKB p2p protocols; must include Sync and Identify.', + source: 'CKB configure docs / CKB protocol definitions', + }, + }, + { + pathPattern: 'network.support_protocols.#', + doc: { + summary: 'One enabled p2p protocol name.', + source: 'CKB protocol definitions', + }, + }, + { + pathPattern: 'miner.client.rpc_url', + doc: { + summary: 'CKB RPC endpoint used by ckb-miner.', + source: 'CKB miner configure docs', + }, + }, + { + pathPattern: 'miner.client.poll_interval', + doc: { + summary: 'Polling interval for new block templates (ms).', + source: 'CKB miner configure docs', + }, + }, + { + pathPattern: 'miner.client.block_on_submit', + doc: { + summary: 'Wait for submit completion before next loop.', + source: 'CKB miner configure docs', + }, + }, +]; + +const FIXED_ARRAY_SPECS: FixedArraySpec[] = [ + { + pathPattern: 'network.support_protocols', + label: 'Network Protocols', + options: [ + 'Ping', + 'Discovery', + 'Identify', + 'Feeler', + 'DisconnectMessage', + 'Sync', + 'Relay', + 'Time', + 'Alert', + 'LightClient', + 'Filter', + ], + unique: true, + allowCustom: false, + source: 'CKB protocol definitions', + }, + { + pathPattern: 'rpc.modules', + label: 'RPC Modules', + options: [ + 'Net', + 'Pool', + 'Miner', + 'Chain', + 'Stats', + 'Experiment', + 'Debug', + 'IntegrationTest', + 'Indexer', + 'Subscription', + ], + unique: true, + allowCustom: true, + source: 'CKB configure docs', + }, +]; + +function splitPattern(pattern: string): string[] { + return pattern.split('.').filter(Boolean); +} + +function isNumericSegment(value: string): boolean { + return /^\d+$/.test(value); +} + +function matchPattern(pathSegments: string[], patternSegments: string[]): boolean { + if (pathSegments.length !== patternSegments.length) { + return false; + } + + return patternSegments.every((patternSegment, index) => { + if (patternSegment === '*') { + return true; + } + + if (patternSegment === '#') { + return isNumericSegment(pathSegments[index]); + } + + return pathSegments[index] === patternSegment; + }); +} + +function wildcardScore(patternSegments: string[]): number { + return patternSegments.filter((segment) => segment === '*' || segment === '#').length; +} + +export function getConfigDoc(pathSegments: string[]): ConfigDoc | null { + const matches = CONFIG_DOCS.filter((rule) => matchPattern(pathSegments, splitPattern(rule.pathPattern))).sort( + (a, b) => { + const aScore = wildcardScore(splitPattern(a.pathPattern)); + const bScore = wildcardScore(splitPattern(b.pathPattern)); + return aScore - bScore; + }, + ); + + if (matches.length === 0) { + return null; + } + + return matches[0].doc; +} + +export function getFixedArraySpec(pathSegments: string[]): FixedArraySpec | null { + const match = FIXED_ARRAY_SPECS.find((spec) => matchPattern(pathSegments, splitPattern(spec.pathPattern))); + return match ?? null; +} + +export function getFixedArraySpecFromEntryPath(pathSegments: string[]): FixedArraySpec | null { + const direct = getFixedArraySpec(pathSegments); + if (direct != null) { + return direct; + } + + if (pathSegments.length === 0) { + return null; + } + + const last = pathSegments[pathSegments.length - 1]; + if (!isNumericSegment(last)) { + return null; + } + + return getFixedArraySpec(pathSegments.slice(0, -1)); +} diff --git a/src/tui/devnet-config-tui.ts b/src/tui/devnet-config-tui.ts new file mode 100644 index 0000000..523efe6 --- /dev/null +++ b/src/tui/devnet-config-tui.ts @@ -0,0 +1,421 @@ +import blessed from 'blessed'; +import path from 'path'; +import { DevnetConfigEditor, TomlEntry } from '../devnet/config-editor'; +import { getFixedArraySpecFromEntryPath } from './devnet-config-metadata'; +import { getEmbeddedReferenceTemplate } from './devnet-reference-templates'; +import { formatEntryLine, formatFixedArrayDetailLine } from './format'; +import { createTuiState, TuiWidgets } from './tui-state'; +import { getListSelected } from './blessed-helpers'; +import { + ActionContext, + editCurrentEntry, + searchEntries, + jumpSearchMatch, + addEntry, + deleteEntry, + insertArrayEntry, + moveArrayEntry, + quitFlow, + saveAndExit, +} from './actions'; + +interface EntryRenderRow { + text: string; + entryIndex: number; + selectable: boolean; +} + +// --------------------------------------------------------------------------- +// Visible-entry filter (compact fixed-array items + search term) +// --------------------------------------------------------------------------- + +function getVisibleEntries(entries: TomlEntry[], searchTerm: string): TomlEntry[] { + const compactEntries = entries.filter((entry) => { + if (entry.path.length === 0) return true; + const lastPathPart = entry.path[entry.path.length - 1]; + const isArrayItem = /^\d+$/.test(lastPathPart); + if (!isArrayItem) return true; + return getFixedArraySpecFromEntryPath(entry.path) == null; + }); + + const term = searchTerm.trim().toLowerCase(); + if (!term) return compactEntries; + return compactEntries.filter((entry) => { + const text = `${entry.pathText} ${entry.valuePreview} ${entry.type}`.toLowerCase(); + return text.includes(term); + }); +} + +function escapeBlessedTags(text: string): string { + return text.replace(/\{/g, '\\{').replace(/\}/g, '\\}'); +} + +function styleTomlReferenceLine(line: string): string { + const escapedLine = escapeBlessedTags(line); + const trimmed = line.trim(); + + if (trimmed.length === 0) { + return escapedLine; + } + + if (/^\[\[[^\]]+\]\]$/.test(trimmed)) { + return `{magenta-fg}{bold}${escapedLine}{/bold}{/magenta-fg}`; + } + + if (/^\[[^\]]+\]$/.test(trimmed)) { + return `{cyan-fg}{bold}${escapedLine}{/bold}{/cyan-fg}`; + } + + if (trimmed.startsWith('#')) { + return `{250-fg}${escapedLine}{/250-fg}`; + } + + const keyValueMatch = line.match(/^(\s*[^=\s][^=]*?\s*=\s*)(.*)$/); + if (keyValueMatch != null) { + const keyPart = escapeBlessedTags(keyValueMatch[1]); + const rawValuePart = keyValueMatch[2] ?? ''; + + const inlineCommentStart = rawValuePart.indexOf(' #'); + if (inlineCommentStart >= 0) { + const valuePart = escapeBlessedTags(rawValuePart.slice(0, inlineCommentStart)); + const commentPart = escapeBlessedTags(rawValuePart.slice(inlineCommentStart)); + return `{yellow-fg}${keyPart}{/yellow-fg}{green-fg}${valuePart}{/green-fg}{250-fg}${commentPart}{/250-fg}`; + } + + const valuePart = escapeBlessedTags(rawValuePart); + return `{yellow-fg}${keyPart}{/yellow-fg}{green-fg}${valuePart}{/green-fg}`; + } + + return escapedLine; +} + +function styleTomlReference(source: string): string { + return source.split('\n').map(styleTomlReferenceLine).join('\n'); +} + +// --------------------------------------------------------------------------- +// Main entry point +// --------------------------------------------------------------------------- + +export async function runDevnetConfigTui(editor: DevnetConfigEditor, configPath: string): Promise { + if (!process.stdin.isTTY || !process.stdout.isTTY) { + throw new Error('Interactive TUI requires a TTY terminal.'); + } + + // ---- state ---- + const state = createTuiState(editor, configPath); + + // ---- widgets ---- + const screen = blessed.screen({ + smartCSR: true, + title: 'OffCKB Devnet Config Editor', + fullUnicode: true, + terminal: 'xterm', + }); + + const filesList = blessed.list({ + parent: screen, + label: ' Files ', + top: 0, + left: 0, + width: '20%', + height: '100%-4', + border: 'line', + keys: true, + vi: true, + style: { selected: { bg: 'blue' }, border: { fg: 'gray' } }, + tags: true, + }); + + const entriesList = blessed.list({ + parent: screen, + label: ' Config ', + top: 0, + left: '20%', + width: '55%', + height: '100%-4', + border: 'line', + keys: true, + vi: true, + style: { selected: { bg: 'blue' }, border: { fg: 'gray' } }, + tags: true, + }); + + const referenceBox = blessed.box({ + parent: screen, + label: ' Reference (Read-Only) ', + top: 0, + left: '75%', + width: '25%', + height: '100%-4', + border: 'line', + scrollable: true, + alwaysScroll: true, + keys: true, + vi: true, + mouse: true, + tags: true, + style: { border: { fg: 'gray' } }, + content: '', + }); + + const statusBar = blessed.box({ + parent: screen, + bottom: 0, + left: 0, + width: '100%', + height: 4, + border: 'line', + tags: true, + content: '', + style: { border: { fg: 'gray' } }, + }); + + const widgets: TuiWidgets = { screen, filesList, entriesList, referenceBox, statusBar }; + let renderedRows: EntryRenderRow[] = []; + let entryToRowIndex: number[] = []; + + // ---- refresh ---- + const refreshUi = () => { + const fileItems = state.documents.map((d) => d.title); + filesList.setItems(fileItems); + filesList.select(state.selectedDocumentIndex); + + const doc = state.documents[state.selectedDocumentIndex]; + const entries = state.editor.getEntriesForDocument(doc.id); + state.visibleEntries = getVisibleEntries(entries, state.searchTerm); + + renderedRows = []; + entryToRowIndex = []; + let hasSeenTopLevelSection = false; + + state.visibleEntries.forEach((entry, entryIndex) => { + if (entry.path.length === 1 && hasSeenTopLevelSection) { + renderedRows.push({ text: ' ', entryIndex: -1, selectable: false }); + } + if (entry.path.length === 1) { + hasSeenTopLevelSection = true; + } + + const entryValue = state.editor.getEntryValue(doc.id, entry.path); + entryToRowIndex[entryIndex] = renderedRows.length; + renderedRows.push({ + text: formatEntryLine(entry, entryValue), + entryIndex, + selectable: true, + }); + + const fixedArraySpec = entry.type === 'array' ? getFixedArraySpecFromEntryPath(entry.path) : null; + if (fixedArraySpec != null && Array.isArray(entryValue)) { + renderedRows.push({ + text: formatFixedArrayDetailLine( + Math.max(0, entry.path.length - 1), + entryValue.map((value) => String(value)), + ), + entryIndex, + selectable: false, + }); + } + }); + + entriesList.setItems(renderedRows.map((row) => row.text)); + + filesList.style.border = { fg: state.focusPane === 'files' ? 'cyan' : 'gray' }; + entriesList.style.border = { fg: state.focusPane === 'entries' ? 'cyan' : 'gray' }; + referenceBox.style.border = { fg: state.focusPane === 'reference' ? 'cyan' : 'gray' }; + + const referenceContent = styleTomlReference(getEmbeddedReferenceTemplate(doc.id)); + referenceBox.setContent(referenceContent); + + if (state.visibleEntries.length === 0 || renderedRows.length === 0) { + state.selectedEntryIndex = 0; + } else { + if (state.selectedEntryIndex >= state.visibleEntries.length) { + state.selectedEntryIndex = state.visibleEntries.length - 1; + } + const selectedRowIndex = entryToRowIndex[state.selectedEntryIndex] ?? 0; + entriesList.select(selectedRowIndex); + } + + const dirtyText = state.hasUnsavedChanges ? '{yellow-fg}yes{/yellow-fg}' : '{green-fg}no{/green-fg}'; + const selectedFilePath = path.join(state.configPath, doc.title); + statusBar.setContent( + [ + `Path: ${selectedFilePath}`, + `Keys: Enter Edit | a Add | d Delete | i Insert | m Move | / Search | n/N Jump | s Save | q Quit | Unsaved: ${dirtyText}`, + ].join('\n'), + ); + + if (state.focusPane === 'files') { + filesList.focus(); + } else if (state.focusPane === 'entries') { + entriesList.focus(); + } else { + referenceBox.focus(); + } + + screen.render(); + }; + + // ---- helpers ---- + const withDialogLock = (fn: () => Promise) => { + if (state.dialogLock) return; + state.dialogLock = true; + fn().finally(() => { + state.dialogLock = false; + }); + }; + + const ctx: ActionContext = { state, widgets, refreshUi }; + + const syncDocumentSelectionFromFilesList = () => { + const listIndex = getListSelected(filesList); + if (listIndex < 0 || listIndex >= state.documents.length) return; + if (listIndex !== state.selectedDocumentIndex) { + state.selectedDocumentIndex = listIndex; + state.selectedEntryIndex = 0; + referenceBox.setScroll(0); + state.statusMessage = `Switched to ${state.documents[state.selectedDocumentIndex].title}.`; + refreshUi(); + } + }; + + const syncEntrySelectionFromEntriesList = () => { + const rowIndex = getListSelected(entriesList); + if (rowIndex < 0 || rowIndex >= renderedRows.length) return; + + let mappedEntryIndex = renderedRows[rowIndex]?.entryIndex ?? -1; + if (mappedEntryIndex < 0) { + for (let i = rowIndex - 1; i >= 0; i--) { + if (renderedRows[i].selectable) { + mappedEntryIndex = renderedRows[i].entryIndex; + break; + } + } + } + if (mappedEntryIndex < 0) { + for (let i = rowIndex + 1; i < renderedRows.length; i++) { + if (renderedRows[i].selectable) { + mappedEntryIndex = renderedRows[i].entryIndex; + break; + } + } + } + + if (mappedEntryIndex < 0 || mappedEntryIndex >= state.visibleEntries.length) return; + if (mappedEntryIndex !== state.selectedEntryIndex) { + state.selectedEntryIndex = mappedEntryIndex; + refreshUi(); + } + }; + + // ---- list events ---- + filesList.on('select', (_: unknown, index: number) => { + if (index == null) return; + state.selectedDocumentIndex = index; + state.selectedEntryIndex = 0; + referenceBox.setScroll(0); + refreshUi(); + }); + + entriesList.on('select', (_: unknown, index: number) => { + if (index == null) return; + syncEntrySelectionFromEntriesList(); + }); + + const NAV_KEYS = ['up', 'down', 'k', 'j', 'pageup', 'pagedown', 'home', 'end']; + + entriesList.on('keypress', (_: unknown, key: { name?: string }) => { + if (!key?.name || !NAV_KEYS.includes(key.name)) return; + setTimeout(() => syncEntrySelectionFromEntriesList(), 0); + }); + + filesList.on('keypress', (_: unknown, key: { name?: string }) => { + if (!key?.name || !NAV_KEYS.includes(key.name)) return; + setTimeout(() => syncDocumentSelectionFromFilesList(), 0); + }); + + // ---- guarded key helpers ---- + const guardedKey = (keys: string[], fn: () => void) => { + screen.key(keys, () => { + if (!state.dialogLock) fn(); + }); + }; + + const guardedKeyAsync = (keys: string[], fn: () => Promise) => { + screen.key(keys, () => { + if (!state.dialogLock) withDialogLock(fn); + }); + }; + + // ---- key bindings ---- + guardedKey(['tab'], () => { + state.focusPane = state.focusPane === 'files' ? 'entries' : state.focusPane === 'entries' ? 'reference' : 'files'; + refreshUi(); + }); + + guardedKey(['left', 'h'], () => { + if (state.focusPane === 'entries') { + state.focusPane = 'files'; + refreshUi(); + return; + } + if (state.focusPane === 'reference') { + state.focusPane = 'entries'; + refreshUi(); + } + }); + + guardedKey(['right', 'l'], () => { + if (state.focusPane === 'files') { + state.focusPane = 'entries'; + refreshUi(); + return; + } + if (state.focusPane === 'entries') { + state.focusPane = 'reference'; + refreshUi(); + } + }); + + guardedKey(['s'], () => saveAndExit(ctx)); + + guardedKeyAsync(['q', 'C-c', 'escape'], () => quitFlow(ctx)); + + guardedKeyAsync(['enter'], () => { + syncEntrySelectionFromEntriesList(); + return editCurrentEntry(ctx); + }); + + guardedKeyAsync(['/'], () => searchEntries(ctx)); + + guardedKeyAsync(['a'], () => { + syncEntrySelectionFromEntriesList(); + return addEntry(ctx); + }); + + guardedKeyAsync(['d'], () => { + syncEntrySelectionFromEntriesList(); + return deleteEntry(ctx); + }); + + guardedKeyAsync(['i'], () => { + syncEntrySelectionFromEntriesList(); + return insertArrayEntry(ctx); + }); + + guardedKeyAsync(['m'], () => { + syncEntrySelectionFromEntriesList(); + return moveArrayEntry(ctx); + }); + + guardedKey(['n'], () => jumpSearchMatch(ctx, 'next')); + guardedKey(['N'], () => jumpSearchMatch(ctx, 'prev')); + + // ---- start ---- + refreshUi(); + + return new Promise((resolve) => { + screen.once('destroy', () => resolve(state.didSave)); + }); +} diff --git a/src/tui/devnet-reference-templates.ts b/src/tui/devnet-reference-templates.ts new file mode 100644 index 0000000..1133ae5 --- /dev/null +++ b/src/tui/devnet-reference-templates.ts @@ -0,0 +1,228 @@ +const CKB_REFERENCE_TEMPLATE = `# Config generated by \`ckb init --chain dev\` + +data_dir = "data" + +[chain] +# Choose the kind of chains to run, possible values: +# - { file = "specs/dev.toml" } +# - { bundled = "specs/testnet.toml" } +# - { bundled = "specs/mainnet.toml" } +spec = { file = "specs/dev.toml" } + +[logger] +filter = "warn,ckb-script=debug" +color = true +log_to_file = true +log_to_stdout = true + +[sentry] +# set to blank to disable sentry error collection +dsn = "" +# if you are willing to help us to improve, +# please leave a way to contact you when we have troubles to reproduce the errors. +# org_contact = "" + +# # **Experimental** Monitor memory changes. +# [memory_tracker] +# # Seconds between checking the process, 0 is disable, default is 0. +# interval = 600 + +[db] +# The capacity of RocksDB cache, which caches uncompressed data blocks, indexes and filters, default is 256MB. +# Rocksdb will automatically create and use an 32MB internal cache for each column family by default if you set this value to 0. +# To turning off cache, you need to set this value to 0 and set \`no_block_cache = true\` in the options_file, +# however, we strongly discourage this setting, it may lead to severe performance degradation. +cache_size = 268435456 + +# Provide an options file to tune RocksDB for your workload and your system configuration. +# More details can be found in [the official tuning guide](https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide). +options_file = "default.db-options" + +[network] +listen_addresses = ["/ip4/0.0.0.0/tcp/8115"] +### Specify the public and routable network addresses +# public_addresses = [] + +# Node connects to nodes listed here to discovery other peers when there's no local stored peers. +# When chain.spec is changed, this usually should also be changed to the bootnodes in the new chain. +bootnodes = [] + +### Whitelist-only mode +# whitelist_only = false +### Whitelist peers connecting from the given IP addresses +# whitelist_peers = [] +### Enable \`SO_REUSEPORT\` feature to reuse port on Linux, not supported on other OS yet +# reuse_port_on_linux = true + +max_peers = 125 +max_outbound_peers = 8 +# 2 minutes +ping_interval_secs = 120 +# 20 minutes +ping_timeout_secs = 1200 +connect_outbound_interval_secs = 15 +# If set to true, try to register upnp +upnp = false +# If set to true, network service will add discovered local address to peer store, it's helpful for private net development +discovery_local_address = true +# If set to true, random cleanup when there are too many inbound nodes +# Ensure that itself can continue to serve as a bootnode node +bootnode_mode = false + +# Supported protocols list, only "Sync" and "Identify" are mandatory, others are optional +support_protocols = ["Ping", "Discovery", "Identify", "Feeler", "DisconnectMessage", "Sync", "Relay", "Time", "Alert", "LightClient", "Filter"] + +# [network.sync.header_map] +# memory_limit = "256MB" + +[rpc] +# By default RPC only binds to localhost, thus it only allows accessing from the same machine. +# +# Allowing arbitrary machines to access the JSON-RPC port is dangerous and strongly discouraged. +# Please strictly limit the access to only trusted machines. +listen_address = "0.0.0.0:8114" + +# Default is 10MiB = 10 * 1024 * 1024 +max_request_body_size = 10485760 + +# List of API modules: ["Net", "Pool", "Miner", "Chain", "Stats", "Subscription", "Experiment", "Debug", "Indexer"] +modules = ["Net", "Pool", "Miner", "Chain", "Stats", "Subscription", "Experiment", "Debug", "Indexer"] + +# By default RPC only binds to HTTP service, you can bind it to TCP and WebSocket. +# tcp_listen_address = "127.0.0.1:18114" +# ws_listen_address = "127.0.0.1:28114" +reject_ill_transactions = true + +# By default deprecated rpc methods are disabled. +enable_deprecated_rpc = false + +[tx_pool] +max_tx_pool_size = 180_000_000 # 180mb +min_fee_rate = 1_000 # Here fee_rate are calculated directly using size in units of shannons/KB +# min_rbf_rate > min_fee_rate means RBF is enabled +min_rbf_rate = 1_500 # Here fee_rate are calculated directly using size in units of shannons/KB +max_tx_verify_cycles = 70_000_000 +max_ancestors_count = 25 + +[store] +header_cache_size = 4096 +cell_data_cache_size = 128 +block_proposals_cache_size = 30 +block_tx_hashes_cache_size = 30 +block_uncles_cache_size = 30 + +# [notifier] +# # Execute command when the new tip block changes, first arg is block hash. +# new_block_notify_script = "your_new_block_notify_script.sh" +# # Execute command when node received an network alert, first arg is alert message string. +# network_alert_notify_script = "your_network_alert_notify_script.sh" + +# Set the lock script to protect mined CKB. +# +# CKB uses CS architecture for miner. Miner process (ckb miner) gets block +# template from the Node process (ckb run) via RPC. Thus the lock script is +# configured in ckb.toml instead of ckb-miner.toml, and the config takes effect +# after restarting Node process. +# +# The \`code_hash\` identifies different cryptography algorithm. Read the manual +# of the lock script provider about how to generate this config. +# +# CKB provides an secp256k1 implementation, it requires a hash on the +# compressed public key. The hash algorithm is blake2b, with personal +# "ckb-default-hash". The first 160 bits (20 bytes) are used as the only arg. +# +# You can use any tool you trust to generate a Bitcoin private key and public +# key pair, which can be used in CKB as well. CKB CLI provides the function for +# you to convert the public key into block assembler configuration parameters. +# +# Here is an example using ckb-cli to generate an account, this command will +# print the block assembler args(lock_arg) to screen: +# +# ckb-cli account new +# +# If you already have a raw secp256k1 private key, you can get the lock_arg by: +# +# ckb-cli util key-info --privkey-path +# +# The command \`ckb init\` also accepts options to generate the block assembler +# directly. See \`ckb init --help\` for details. +# +# ckb init +# +# secp256k1_blake160_sighash_all example: +# [block_assembler] +# code_hash = "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8" +# args = "ckb-cli util blake2b --prefix-160 " +# hash_type = "type" +# message = "A 0x-prefixed hex string" +# # +# # CKB will prepend the binary version to message, to identify the block miner client. (default true, false to disable it) +# use_binary_version_as_message_prefix = true +# # +# # Block assembler will notify new block template through http post to specified endpoints when update +# notify = ["http://127.0.0.1:8888"] +# # Or you may want use more flexible scripts, block template as arg. +# notify_scripts = ["{cmd} {blocktemplate}"] +# +# [indexer_v2] +# # Indexing the pending txs in the ckb tx-pool +# index_tx_pool = false + +# ckb miner / ckb faucet +# private key: 0x650e256211f5e0beee9084596aa2cb84d11eb033cced5e2d5b191593a9f9f1d4 +# private key path: accounts/ckb-miner-and-faucet.key +[block_assembler] +code_hash = "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8" +args = "0xa1db2eef3f29f3ef6f86c8d2a0772c705c449f4a" +hash_type = "type" +message = "0x" +`; + +const MINER_REFERENCE_TEMPLATE = `# Config generated by \`ckb init --chain dev\` + +data_dir = "data" + +[chain] +# Choose the kind of chains to run, possible values: +# - { file = "specs/dev.toml" } +# - { bundled = "specs/testnet.toml" } +# - { bundled = "specs/mainnet.toml" } +spec = { file = "specs/dev.toml" } + +[logger] +filter = "warn,ckb-script=debug" +color = true +log_to_file = true +log_to_stdout = true + +[sentry] +# set to blank to disable sentry error collection +dsn = "" +# if you are willing to help us to improve, +# please leave a way to contact you when we have troubles to reproduce the errors. +# org_contact = "" + +# # **Experimental** Monitor memory changes. +# [memory_tracker] +# # Seconds between checking the process, 0 is disable, default is 0. +# interval = 600 + +[miner.client] +rpc_url = "http://ckb:8114/" +block_on_submit = true + +# block template polling interval in milliseconds +poll_interval = 1000 + +# enable listen notify mode +# listen = "127.0.0.1:8888" + +[[miner.workers]] +worker_type = "Dummy" +delay_type = "Constant" +value = 5000 +`; + +export function getEmbeddedReferenceTemplate(documentId: 'ckb' | 'miner'): string { + return documentId === 'ckb' ? CKB_REFERENCE_TEMPLATE : MINER_REFERENCE_TEMPLATE; +} diff --git a/src/tui/dialogs.ts b/src/tui/dialogs.ts new file mode 100644 index 0000000..448faab --- /dev/null +++ b/src/tui/dialogs.ts @@ -0,0 +1,550 @@ +import blessed, { Widgets } from 'blessed'; +import { FixedArraySpec } from './devnet-config-metadata'; +import { getListSelected } from './blessed-helpers'; + +// --------------------------------------------------------------------------- +// Fixed-array multi-select dialog +// --------------------------------------------------------------------------- + +export async function waitForFixedArraySelection( + screen: Widgets.Screen, + title: string, + spec: FixedArraySpec, + currentValues: string[], +): Promise { + return new Promise((resolve) => { + const knownOptions = [...spec.options]; + const customCurrentValues = currentValues.filter((value) => !knownOptions.includes(value)); + const optionList = [...knownOptions, ...customCurrentValues]; + + const maxVisibleRows = 14; + const visibleRows = Math.min(Math.max(optionList.length, 1), maxVisibleRows); + const listHeight = visibleRows + 2; + const dialogHeight = listHeight + 6; + + const overlay = blessed.box({ + parent: screen, + top: 0, + left: 0, + width: '100%', + height: '100%', + mouse: true, + keys: true, + tags: true, + }); + + const dialog = blessed.box({ + parent: overlay, + label: ` ${title} `, + border: 'line', + top: 'center', + left: 'center', + width: '62%', + height: dialogHeight, + mouse: true, + keys: true, + tags: true, + style: { border: { fg: 'cyan' } }, + }); + + const selectedValues = new Set(currentValues); + + const list = blessed.list({ + parent: dialog, + top: 1, + left: 1, + width: '100%-2', + height: listHeight, + border: 'line', + mouse: true, + keys: true, + vi: true, + tags: true, + style: { + selected: { bg: 'blue' }, + border: { fg: 'gray' }, + }, + }); + + blessed.box({ + parent: dialog, + top: listHeight + 1, + left: 2, + width: '100%-4', + height: 2, + tags: true, + content: `Space toggle Enter apply Esc cancel Ctrl/Alt+a all Ctrl/Alt+d none${spec.allowCustom ? ' c add custom' : ''}`, + }); + + const renderList = () => { + const items = optionList.map((option) => { + const checked = selectedValues.has(option) ? 'x' : ' '; + const suffix = knownOptions.includes(option) ? '' : ' (custom)'; + return `[${checked}] ${option}${suffix}`; + }); + list.setItems(items); + }; + + renderList(); + list.select(0); + + const screenWithGrab = screen as unknown as { grabKeys?: boolean; grabMouse?: boolean }; + const previousGrabKeys = screenWithGrab.grabKeys; + const previousGrabMouse = screenWithGrab.grabMouse; + screenWithGrab.grabKeys = true; + screenWithGrab.grabMouse = true; + + let resolved = false; + const cleanup = (value: string[] | null) => { + if (resolved) return; + resolved = true; + screenWithGrab.grabKeys = previousGrabKeys; + screenWithGrab.grabMouse = previousGrabMouse; + overlay.destroy(); + screen.render(); + resolve(value); + }; + + const selectedOption = () => { + const selectedIndex = getListSelected(list); + return optionList[selectedIndex] ?? null; + }; + + const applySelection = () => { + const values = optionList.filter((option) => selectedValues.has(option)); + cleanup(values); + }; + + const toggleSelectedOption = () => { + const option = selectedOption(); + if (option == null) return; + + if (selectedValues.has(option)) { + selectedValues.delete(option); + } else { + selectedValues.add(option); + } + renderList(); + list.select(getListSelected(list)); + screen.render(); + }; + + list.key(['enter'], () => applySelection()); + list.key(['space'], () => toggleSelectedOption()); + list.key(['escape'], () => cleanup(null)); + list.key(['C-a', 'A-a', 'M-a'], () => { + optionList.forEach((option) => selectedValues.add(option)); + renderList(); + screen.render(); + }); + list.key(['C-d', 'A-d', 'M-d'], () => { + selectedValues.clear(); + renderList(); + screen.render(); + }); + + dialog.key(['escape'], () => cleanup(null)); + dialog.key(['C-a', 'A-a', 'M-a'], () => { + optionList.forEach((option) => selectedValues.add(option)); + renderList(); + screen.render(); + }); + dialog.key(['C-d', 'A-d', 'M-d'], () => { + selectedValues.clear(); + renderList(); + screen.render(); + }); + + const addCustomValue = async () => { + if (!spec.allowCustom) return; + + const answer = await waitForInput(screen, `${title} (${spec.label})`, 'Custom value:', ''); + if (answer == null) { + list.focus(); + screen.render(); + return; + } + + const customValue = answer.trim(); + if (!customValue) { + list.focus(); + screen.render(); + return; + } + + if (!optionList.includes(customValue)) { + optionList.push(customValue); + } + selectedValues.add(customValue); + + renderList(); + const index = optionList.findIndex((option) => option === customValue); + if (index >= 0) { + list.select(index); + } + list.focus(); + screen.render(); + }; + + list.key(['c'], () => { + void addCustomValue(); + }); + + dialog.key(['c'], () => { + void addCustomValue(); + }); + + list.focus(); + screen.render(); + }); +} + +// --------------------------------------------------------------------------- +// Text input dialog +// --------------------------------------------------------------------------- + +export function waitForInput( + screen: Widgets.Screen, + title: string, + questionText: string, + initialValue: string, +): Promise { + return new Promise((resolve) => { + const dialog = blessed.box({ + parent: screen, + label: ` ${title} `, + border: 'line', + top: 'center', + left: 'center', + width: '70%', + height: 13, + keys: true, + tags: true, + style: { border: { fg: 'cyan' } }, + }); + + blessed.box({ + parent: dialog, + top: 1, + left: 2, + width: '95%-4', + height: 2, + tags: true, + content: questionText, + }); + + const input = blessed.textbox({ + parent: dialog, + top: 3, + left: 2, + width: '100%-4', + height: 3, + border: 'line', + inputOnFocus: true, + keys: true, + vi: true, + style: { border: { fg: 'gray' } }, + }); + + const okButton = blessed.button({ + parent: dialog, + mouse: true, + keys: true, + shrink: true, + top: 8, + left: '40%-8', + height: 1, + content: ' OK ', + style: { bg: 'blue', focus: { bg: 'blue' } }, + }); + + const cancelButton = blessed.button({ + parent: dialog, + mouse: true, + keys: true, + shrink: true, + top: 8, + left: '40%+4', + height: 1, + content: ' Cancel ', + style: { bg: 'gray', focus: { bg: 'gray' } }, + }); + + type InputDialogFocus = 'input' | 'ok' | 'cancel'; + let currentFocus: InputDialogFocus = 'input'; + + let resolved = false; + const cleanup = (value: string | null) => { + if (resolved) return; + resolved = true; + dialog.destroy(); + screen.render(); + resolve(value); + }; + + const setFocus = (nextFocus: InputDialogFocus) => { + if (resolved) return; + currentFocus = nextFocus; + if (nextFocus === 'input') { + input.style.border = { fg: 'cyan' }; + okButton.style.bg = 'blue'; + cancelButton.style.bg = 'gray'; + input.focus(); + } else if (nextFocus === 'ok') { + input.style.border = { fg: 'gray' }; + okButton.style.bg = 'cyan'; + cancelButton.style.bg = 'gray'; + okButton.focus(); + } else { + input.style.border = { fg: 'gray' }; + okButton.style.bg = 'blue'; + cancelButton.style.bg = 'cyan'; + cancelButton.focus(); + } + screen.render(); + }; + + const nextFocus = () => { + if (currentFocus === 'input') { + setFocus('ok'); + return; + } + if (currentFocus === 'ok') { + setFocus('cancel'); + return; + } + setFocus('input'); + }; + + const prevFocus = () => { + if (currentFocus === 'cancel') { + setFocus('ok'); + return; + } + if (currentFocus === 'ok') { + setFocus('input'); + return; + } + setFocus('cancel'); + }; + + const getInputValue = () => input.getValue() ?? ''; + + okButton.on('press', () => cleanup(getInputValue())); + cancelButton.on('press', () => cleanup(null)); + + dialog.key(['escape'], () => cleanup(null)); + dialog.key(['tab', 'down'], () => nextFocus()); + dialog.key(['S-tab', 'up'], () => prevFocus()); + dialog.key(['left'], () => { + if (currentFocus !== 'input') prevFocus(); + }); + dialog.key(['right'], () => { + if (currentFocus !== 'input') nextFocus(); + }); + dialog.key(['enter'], () => { + if (currentFocus === 'input') { + cleanup(getInputValue()); + return; + } + if (currentFocus === 'ok') { + cleanup(getInputValue()); + return; + } + cleanup(null); + }); + + input.key(['enter'], () => cleanup(getInputValue())); + + input.setValue(initialValue); + setFocus('input'); + input.readInput(); + screen.render(); + }); +} + +// --------------------------------------------------------------------------- +// Confirmation dialog +// --------------------------------------------------------------------------- + +export function waitForConfirm( + screen: Widgets.Screen, + title: string, + text: string, + options?: { + confirmLabel?: string; + cancelLabel?: string; + defaultFocus?: 'confirm' | 'cancel'; + }, +): Promise { + return new Promise((resolve) => { + const confirmLabel = options?.confirmLabel ?? 'OK'; + const cancelLabel = options?.cancelLabel ?? 'Cancel'; + const defaultFocus: 'ok' | 'cancel' = options?.defaultFocus === 'confirm' ? 'ok' : 'cancel'; + const buttonGap = 3; + const buttonWidth = Math.max(confirmLabel.length, cancelLabel.length) + 4; + const leftHalfOffset = buttonWidth + Math.ceil(buttonGap / 2); + const rightHalfOffset = Math.floor(buttonGap / 2); + + const dialog = blessed.box({ + parent: screen, + label: ` ${title} `, + border: 'line', + top: 'center', + left: 'center', + width: '60%', + height: 10, + keys: true, + tags: true, + style: { border: { fg: 'cyan' } }, + }); + + blessed.box({ + parent: dialog, + top: 2, + left: 2, + width: '100%-4', + height: 2, + tags: true, + content: text, + }); + + const okButton = blessed.button({ + parent: dialog, + mouse: true, + keys: true, + shrink: false, + top: 5, + left: `50%-${leftHalfOffset}`, + width: buttonWidth, + height: 1, + content: ` ${confirmLabel} `, + style: { bg: 'blue', focus: { bg: 'blue' } }, + }); + + const cancelButton = blessed.button({ + parent: dialog, + mouse: true, + keys: true, + shrink: false, + top: 5, + left: `50%+${rightHalfOffset}`, + width: buttonWidth, + height: 1, + content: ` ${cancelLabel} `, + style: { bg: 'gray', focus: { bg: 'gray' } }, + }); + + const focusNavKeys = ['tab', 'right', 'left', 'S-tab']; + const confirmKeys = ['enter', 'return', 'C-m']; + + let focusButton: 'ok' | 'cancel' = 'cancel'; + + let resolved = false; + const cleanup = (answer: boolean) => { + if (resolved) return; + resolved = true; + dialog.destroy(); + screen.render(); + resolve(answer); + }; + + const applyFocusStyles = (focus: 'ok' | 'cancel') => { + if (focus === 'ok') { + okButton.style.bg = 'cyan'; + cancelButton.style.bg = 'gray'; + } else { + okButton.style.bg = 'blue'; + cancelButton.style.bg = 'cyan'; + } + }; + + const setFocus = (focus: 'ok' | 'cancel') => { + if (resolved) return; + focusButton = focus; + applyFocusStyles(focus); + if (focus === 'ok') { + okButton.focus(); + } else { + cancelButton.focus(); + } + screen.render(); + }; + + const toggleFocus = () => { + setFocus(focusButton === 'ok' ? 'cancel' : 'ok'); + }; + + okButton.on('focus', () => { + if (resolved) return; + focusButton = 'ok'; + applyFocusStyles('ok'); + screen.render(); + }); + + cancelButton.on('focus', () => { + if (resolved) return; + focusButton = 'cancel'; + applyFocusStyles('cancel'); + screen.render(); + }); + + const accept = () => cleanup(true); + const cancel = () => cleanup(false); + + okButton.on('press', () => accept()); + cancelButton.on('press', () => cancel()); + + dialog.key(['escape'], () => cancel()); + dialog.key(['tab', 'left', 'right'], () => toggleFocus()); + dialog.key(['S-tab'], () => toggleFocus()); + dialog.key(confirmKeys, () => { + if (focusButton === 'ok') { + accept(); + } else { + cancel(); + } + }); + + const buttons = [okButton, cancelButton]; + buttons.forEach((button) => { + button.key(focusNavKeys, () => toggleFocus()); + button.key(['escape'], () => cancel()); + }); + okButton.key(confirmKeys, () => accept()); + cancelButton.key(confirmKeys, () => cancel()); + + setFocus(defaultFocus); + screen.render(); + }); +} + +// --------------------------------------------------------------------------- +// Array-value input (routes to fixed-array dialog or text input) +// --------------------------------------------------------------------------- + +export async function waitForArrayValue( + screen: Widgets.Screen, + spec: FixedArraySpec | null, + title: string, + questionText: string, + initialValue: string, +): Promise { + if (spec == null) { + return waitForInput(screen, title, questionText, initialValue); + } + + if (spec.options.includes(initialValue)) { + const selected = await waitForFixedArraySelection(screen, `${title} (${spec.label})`, spec, [initialValue]); + if (selected == null || selected.length === 0) return null; + return selected[0]; + } + + if (!spec.allowCustom) { + const selected = await waitForFixedArraySelection(screen, `${title} (${spec.label})`, spec, []); + if (selected == null || selected.length === 0) return null; + return selected[0]; + } + + return waitForInput(screen, title, questionText, initialValue); +} diff --git a/src/tui/format.ts b/src/tui/format.ts new file mode 100644 index 0000000..6053c0d --- /dev/null +++ b/src/tui/format.ts @@ -0,0 +1,37 @@ +import { TomlEntry } from '../devnet/config-editor'; +import { getConfigDoc } from './devnet-config-metadata'; + +function formatFixedArrayInline(values: string[]): string { + if (values.length === 0) { + return '{light-cyan-fg}[]{/light-cyan-fg}'; + } + + return `{light-cyan-fg}[${values.join(', ')}]{/light-cyan-fg}`; +} + +export function formatFixedArrayDetailLine(depth: number, values: string[]): string { + const detailIndent = `${'│ '.repeat(Math.max(0, depth))} `; + return `${detailIndent}${formatFixedArrayInline(values)}`; +} + +export function formatEntryLine(entry: TomlEntry, _entryValue?: unknown): string { + const depth = Math.max(0, entry.path.length - 1); + const lastPathPart = entry.path[entry.path.length - 1] ?? ''; + const nodeName = /^\d+$/.test(lastPathPart) ? `[${lastPathPart}]` : lastPathPart; + const treeIndent = depth === 0 ? '' : `${'│ '.repeat(Math.max(0, depth - 1))}`; + const branch = depth === 0 ? '' : '├─ '; + const keyDoc = getConfigDoc(entry.path); + const docText = keyDoc != null ? ` {243-fg}// ${keyDoc.summary}{/243-fg}` : ''; + const valueColor = entry.type === 'string' ? 'green' : entry.type === 'number' ? 'yellow' : 'magenta'; + const keyColor = depth === 0 ? 'cyan' : 'white'; + + if (entry.type === 'object') { + return `${treeIndent}${branch}{cyan-fg}▸ ${nodeName}{/cyan-fg} {gray-fg}${entry.valuePreview}{/gray-fg}${docText}`; + } + + if (entry.type === 'array') { + return `${treeIndent}${branch}{magenta-fg}▾ ${nodeName}{/magenta-fg} {white-fg}${entry.valuePreview}{/white-fg}${docText}`; + } + + return `${treeIndent}${branch}{${keyColor}-fg}${nodeName}{/${keyColor}-fg} = {${valueColor}-fg}${entry.valuePreview}{/${valueColor}-fg}${docText}`; +} diff --git a/src/tui/tui-state.ts b/src/tui/tui-state.ts new file mode 100644 index 0000000..db85986 --- /dev/null +++ b/src/tui/tui-state.ts @@ -0,0 +1,44 @@ +import { Widgets } from 'blessed'; +import { DevnetConfigEditor, TomlDocument, TomlEntry } from '../devnet/config-editor'; + +export type FocusPane = 'files' | 'entries' | 'reference'; + +export interface TuiState { + readonly editor: DevnetConfigEditor; + readonly configPath: string; + readonly documents: TomlDocument[]; + selectedDocumentIndex: number; + selectedEntryIndex: number; + focusPane: FocusPane; + hasUnsavedChanges: boolean; + didSave: boolean; + searchTerm: string; + statusMessage: string; + visibleEntries: TomlEntry[]; + dialogLock: boolean; +} + +export interface TuiWidgets { + screen: Widgets.Screen; + filesList: Widgets.ListElement; + entriesList: Widgets.ListElement; + referenceBox: Widgets.BoxElement; + statusBar: Widgets.BoxElement; +} + +export function createTuiState(editor: DevnetConfigEditor, configPath: string): TuiState { + return { + editor, + configPath, + documents: editor.getDocuments(), + selectedDocumentIndex: 0, + selectedEntryIndex: 0, + focusPane: 'files', + hasUnsavedChanges: false, + didSave: false, + searchTerm: '', + statusMessage: 'Ready', + visibleEntries: [], + dialogLock: false, + }; +} diff --git a/templates/readme.md b/templates/readme.md index 64a07ae..7932262 100644 --- a/templates/readme.md +++ b/templates/readme.md @@ -1 +1 @@ -All the templates are located in the `v3` like folder, where `v3` represent your offckb version. +All the templates are located in the `v4` folder, representing the current offckb template version. diff --git a/templates/v3/js-script-next-js/README.md b/templates/v3/js-script-next-js/README.md deleted file mode 100644 index a17be74..0000000 --- a/templates/v3/js-script-next-js/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# offckb-template - -This is a Minimal Template for CKB Full-Stack Dapps generated by [offckb](https://github.com/RetricSu/offckb). - -Offckb does not do the magic. It just wraps the new CKB smart contract template and the CKB javascript Dapp framework into one mono-repo. Under the hook, it uses: - -- [ckb-js-vm](https://github.com/nervosnetwork/ckb-js-vm) for smart contract development -- [next-js](https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app) and [ccc](https://github.com/ckb-devrel/ccc) for frontend development - -To start the project, install the dependecies: - -```bash -pnpm i -``` - -## Script(Smart Contract) - -The on-chain script project is powered by [ckb-js-vm](https://github.com/nervosnetwork/ckb-js-vm) bootstrapped with [`create-ckb-js-vm-app`](https://github.com/nervosnetwork/ckb-js-vm). - -Build on-chain script: - -```bash -pnpm build -``` - -Test on-chain script: - -```bash -pnpm test -``` - -## dApp frontend development - -first, enter the frontend workspace: - -```sh -cd frontend -``` - -start the app: - -```sh -pnpm dev -``` - -change the CKB blockchain network: - -edit `.env` file: - -```bash -NEXT_PUBLIC_NETWORK=devnet # devnet, testnet or mainnet -``` - -## Deploy to devnet/testnet with offckb - -Once you build your smart contracts, you can deploy them to CKB blockchain with [ckb-cli](https://github.com/nervosnetwork/ckb-cli) or any other tools. - -If you want to test them in devnet/testnet blockchain, then `offckb` might be the ideal selection. - -`offckb` will look for the `offckb.config.ts` file to read config information. so you will need to enter the frontend workspace to do the instruction: - -```sh -cd frontend -offckb deploy --network devnet -``` - -If successfully deployed, you will see the deploy script info for your smart contract recorded in the path recorded in the `offckb.config.ts` file. - -Every time you deploy a new version of your smart contracts, those script infos will be updated by `offckb` in the place recorded in `offckb.config.ts` and work out-of-box in your frontend. - -You can also deploy smart contracts to the CKB Testnet like this: - -```sh -cd frontend -offckb deploy --network testnet -``` - -and start your frontend Dapp targeting Testnet: - -edit `.env` file: - -```bash -NEXT_PUBLIC_NETWORK=testnet # devnet, testnet or mainnet -``` - -```bash -cd frontend -pnpm dev -``` - -Note that the `mainnet` network is not supported in `offckb` since `offckb` is focusing on building a friendly development environment for CKB. To gain better security, it is recommended to use production tools like [ckb-cli](https://github.com/nervosnetwork/ckb-cli) to deploy smart contracts and do transactions for the CKB mainnet. diff --git a/templates/v3/js-script-next-js/package.json b/templates/v3/js-script-next-js/package.json deleted file mode 100644 index 57b453a..0000000 --- a/templates/v3/js-script-next-js/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "private": true, - "workspaces": [ - "packages/*" - ], - "scripts": { - "build": "pnpm -r run build", - "clean": "pnpm -r run clean", - "test": "pnpm -r run test", - "format": "pnpm -r run format" - }, - "devDependencies": { - "prettier": "~3.5.3", - "prettier-plugin-organize-imports": "~4.1.0", - "rimraf": "~6.0.1", - "typescript": "~5.8.2" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild" - ], - "overrides": { - "brace-expansion@>=1.0.0 <=1.1.11": ">=1.1.12", - "brace-expansion@>=2.0.0 <=2.0.1": ">=2.0.2", - "axios@>=1.0.0 <1.12.0": ">=1.12.0", - "sha.js@<=2.4.11": ">=2.4.12", - "form-data@>=4.0.0 <4.0.4": ">=4.0.4" - } - } -} diff --git a/templates/v3/js-script-next-js/packages/frontend/.env b/templates/v3/js-script-next-js/packages/frontend/.env deleted file mode 100644 index fdea672..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_NETWORK=devnet diff --git a/templates/v3/js-script-next-js/packages/frontend/.eslintrc.json b/templates/v3/js-script-next-js/packages/frontend/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/templates/v3/js-script-next-js/packages/frontend/.gitignore b/templates/v3/js-script-next-js/packages/frontend/.gitignore deleted file mode 100644 index fd3dbb5..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/templates/v3/js-script-next-js/packages/frontend/README.md b/templates/v3/js-script-next-js/packages/frontend/README.md deleted file mode 100644 index 788b31b..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/README.md +++ /dev/null @@ -1,44 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Targeting on Different CKB Networks - -edit `.env` file: - -```bash -NEXT_PUBLIC_NETWORK=devnet # devnet, testnet or mainnet -``` - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/templates/v3/js-script-next-js/packages/frontend/app/favicon.ico b/templates/v3/js-script-next-js/packages/frontend/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/templates/v3/js-script-next-js/packages/frontend/app/favicon.ico and /dev/null differ diff --git a/templates/v3/js-script-next-js/packages/frontend/app/globals.css b/templates/v3/js-script-next-js/packages/frontend/app/globals.css deleted file mode 100644 index 2ac8372..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/app/globals.css +++ /dev/null @@ -1,7 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -a { - @apply underline text-blue-600 hover:text-blue-800 visited:text-purple-600; -} diff --git a/templates/v3/js-script-next-js/packages/frontend/app/layout.tsx b/templates/v3/js-script-next-js/packages/frontend/app/layout.tsx deleted file mode 100644 index 637a925..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/app/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; -import './globals.css'; - -const inter = Inter({ subsets: ['latin'] }); - -export const metadata: Metadata = { - title: 'CKB Next APP', - description: 'Generated by create next app', -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/templates/v3/js-script-next-js/packages/frontend/app/page.tsx b/templates/v3/js-script-next-js/packages/frontend/app/page.tsx deleted file mode 100644 index 97ddece..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/app/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import offckb from '@/offckb.config'; -import { ccc } from '@ckb-ccc/connector-react'; -import Wallet from './wallet'; - -export default function Home() { - const isScriptDeployed = offckb.myScripts['hello-world.bc'] != null; - return ( - -
-
Minimal Template for CKB DApp
- - -
-
Tech Stack
-
  • - - ckb-js-std & ckb-js-vm - {' '} - for smart contract development in Typescript -
  • -
  • - - Next.js - {' '} - for Javascript frontend framework -
  • -
  • - - CCC - {' '} - for off-chain CKB SDK -
  • -
    - -
    -
    CKB Blockchain
    -
  • - Current Network: {offckb.currentNetwork}, Address Prefix: {offckb.addressPrefix} -
  • -
  • - CKB RPC URL:{' '} - - {offckb.rpcUrl} - -
  • -
  • - Switch different networks with .env{' '} - - NEXT_PUBLIC_NETWORK - -
  • -
    - -
    -
    Smart Contract
    -
    - hello-world Script{' '} - {isScriptDeployed ? ( -
    -
  • code_hash: {offckb.myScripts['hello-world.bc']?.codeHash}
  • -
  • hash_type: {offckb.myScripts['hello-world.bc']?.hashType}
  • -
  • - outpoint: {offckb.myScripts['hello-world.bc']?.cellDeps[0].cellDep.outPoint.txHash}: - {offckb.myScripts['hello-world.bc']?.cellDeps[0].cellDep.outPoint.index} -
  • -
    - ) : ( - - Not Found,{' '} - - deploy - {' '} - it first. - - )} -
    -
    - -
    -
    Wallet Connector
    -
    - -
    -
    - -
    -
    - This template is created by{' '} - - offckb - -
    -
    -
    - ); -} diff --git a/templates/v3/js-script-next-js/packages/frontend/app/wallet-client.ts b/templates/v3/js-script-next-js/packages/frontend/app/wallet-client.ts deleted file mode 100644 index c4afd3b..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/app/wallet-client.ts +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { ccc, CellDepInfoLike, KnownScript, Script } from '@ckb-ccc/connector-react'; -import offCKBConfig, { Network } from '@/offckb.config'; - -export const DEVNET_SCRIPTS: Record & { cellDeps: CellDepInfoLike[] }> = { - [KnownScript.Secp256k1Blake160]: offCKBConfig.systemScripts.secp256k1_blake160_sighash_all!.script, - [KnownScript.Secp256k1Multisig]: offCKBConfig.systemScripts.secp256k1_blake160_multisig_all!.script, - [KnownScript.AnyoneCanPay]: offCKBConfig.systemScripts.anyone_can_pay!.script, - [KnownScript.OmniLock]: offCKBConfig.systemScripts.omnilock!.script, - [KnownScript.XUdt]: offCKBConfig.systemScripts.xudt!.script, -}; - -export function buildCccClient(network: Network) { - const client = - network === 'mainnet' - ? new ccc.ClientPublicMainnet() - : network === 'testnet' - ? new ccc.ClientPublicTestnet() - : new ccc.ClientPublicTestnet({ url: offCKBConfig.rpcUrl, scripts: DEVNET_SCRIPTS }); - - return client; -} diff --git a/templates/v3/js-script-next-js/packages/frontend/app/wallet.tsx b/templates/v3/js-script-next-js/packages/frontend/app/wallet.tsx deleted file mode 100644 index 8155ec4..0000000 --- a/templates/v3/js-script-next-js/packages/frontend/app/wallet.tsx +++ /dev/null @@ -1,181 +0,0 @@ -'use client'; - -import { ccc } from '@ckb-ccc/connector-react'; -import React, { useEffect, useState } from 'react'; -import { readEnvNetwork } from '@/offckb.config'; -import { buildCccClient } from './wallet-client'; - -function WalletIcon({ wallet, className }: { wallet: ccc.Wallet; className?: string }) { - return ( - // eslint-disable-next-line @next/next/no-img-element - {wallet.name} - ); -} - -function Button(props: React.ButtonHTMLAttributes) { - return ( - - - - ); -} - -function Transfer() { - const signer = ccc.useSigner(); - const [transferTo, setTransferTo] = useState(''); - const [amount, setAmount] = useState(''); - const [hash, setHash] = useState(''); - const [data, setData] = useState(''); - - return ( -
    - {hash !== '' ?

    {hash}

    : <>} -
    -
    - setTransferTo(e.currentTarget.value)} - placeholder="Enter address to transfer to" - /> - setAmount(e.currentTarget.value)} - placeholder="Enter amount in CKB to transfer" - /> -