diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24b515c..f835318 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,9 @@ name: Test Flow on: pull_request: - branches: - - main + +permissions: + contents: read jobs: build: @@ -42,6 +43,57 @@ jobs: - name: Run lint run: npm run lint + pack_smoke: + # Guards the global-install contract without needing a full Ocean stack: + # a broken bin, cwd-dependent module loading, or env-gated --help/--version + # would all fail here. Also asserts the publish tarball excludes sources. + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.5.1" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Pack tarball + run: | + TARBALL=$(npm pack | tail -1) + echo "TARBALL=$TARBALL" >> $GITHUB_ENV + echo "Packed $TARBALL" + + - name: Assert tarball contents (dist/metadata/README in; src/test out) + run: | + FILES=$(tar -tzf "$TARBALL") + echo "$FILES" + echo "$FILES" | grep -q '^package/dist/index.js$' || { echo "MISSING dist/index.js"; exit 1; } + echo "$FILES" | grep -q '^package/README.md$' || { echo "MISSING README.md"; exit 1; } + echo "$FILES" | grep -q '^package/metadata/' || { echo "MISSING metadata/"; exit 1; } + if echo "$FILES" | grep -qE '^package/(src|test)/'; then + echo "Tarball must not ship src/ or test/"; exit 1 + fi + + - name: Install globally from tarball + run: npm i -g "./$TARBALL" + + - name: Run bin from a foreign cwd with NO env vars + working-directory: ${{ runner.temp }} + run: | + ocean-cli --version + ocean-cli --help > /dev/null + ocean-cli -h > /dev/null + ocean-cli -V > /dev/null + test_system: runs-on: ubuntu-latest strategy: @@ -75,6 +127,7 @@ jobs: with: repository: 'oceanprotocol/barge' path: 'barge' + ref: "feature/node-v4" - name: Login to Docker Hub if: ${{ env.DOCKERHUB_PASSWORD && env.DOCKERHUB_USERNAME }} @@ -89,6 +142,9 @@ jobs: working-directory: ${{ github.workspace }}/barge run: | bash -x start_ocean.sh --with-typesense 2>&1 > start_ocean.log & + env: + CONTRACTS_VERSION: '2.9.0' + NODE_VERSION: "pr-1408" - run: npm ci - run: npm run build - run: docker image ls diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..65ad8da --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,54 @@ +name: Publish + +on: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write # required for npm provenance + +jobs: + npm: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.5.1" + registry-url: "https://registry.npmjs.org/" + + # Only publish a tag whose version matches package.json, so a stray or + # mistyped tag can never publish a mismatched release. + - name: Verify tag matches package.json version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PKG_VERSION="$(node -p "require('./package.json').version")" + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match package.json version $PKG_VERSION" + exit 1 + fi + + - name: Install dependencies + run: npm ci + + # prepublishOnly (npm run build) runs automatically during npm publish. + # --access public is required for a scoped package; --provenance attests + # the build to the source commit (needs the package.json `repository` + # field to match this repo and the id-token permission above). + - name: Publish (next) + if: contains(github.ref, 'next') + run: npm publish --tag next --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish (latest) + if: ${{ !contains(github.ref, 'next') }} + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..12a1e3b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,113 @@ +### Changelog + +All notable changes to this project will be documented in this file. Dates are displayed in UTC. + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +#### [v2.0.0-next.4](https://github.com/oceanprotocol/ocean-cli/compare/v2.0.0-next.3...v2.0.0-next.4) + +- fixes [`b7b7754`](https://github.com/oceanprotocol/ocean-cli/commit/b7b7754fa881f857358c00f2c2ba8aa2b3ac68e7) + +#### [v2.0.0-next.3](https://github.com/oceanprotocol/ocean-cli/compare/v2.0.0-next.2...v2.0.0-next.3) + +> 24 July 2026 + +- Release v2.0.0-next.3 [`e7c4e13`](https://github.com/oceanprotocol/ocean-cli/commit/e7c4e13fc8314233d52145e4db306048cc4d404e) +- fix package url [`1f58c82`](https://github.com/oceanprotocol/ocean-cli/commit/1f58c82aba84a9e7b0b49e3e328024c16441ffc8) + +#### [v2.0.0-next.2](https://github.com/oceanprotocol/ocean-cli/compare/v1.0.0...v2.0.0-next.2) + +> 24 July 2026 + +- fix interactive menu [`#162`](https://github.com/oceanprotocol/ocean-cli/pull/162) +- ddoJs for v5 [`#131`](https://github.com/oceanprotocol/ocean-cli/pull/131) +- Feature/add claude [`#158`](https://github.com/oceanprotocol/ocean-cli/pull/158) +- codeowner [`#157`](https://github.com/oceanprotocol/ocean-cli/pull/157) +- Feature/allow full compute asset [`#156`](https://github.com/oceanprotocol/ocean-cli/pull/156) +- Change order initial users param [`#155`](https://github.com/oceanprotocol/ocean-cli/pull/155) +- Bucket methods [`#154`](https://github.com/oceanprotocol/ocean-cli/pull/154) +- bump ocean.js + ddo.js [`#152`](https://github.com/oceanprotocol/ocean-cli/pull/152) +- ocean.js p2p [`#151`](https://github.com/oceanprotocol/ocean-cli/pull/151) +- feat(#148): expose external storage c2d [`#150`](https://github.com/oceanprotocol/ocean-cli/pull/150) +- use latest ocean.js [`#147`](https://github.com/oceanprotocol/ocean-cli/pull/147) +- chore: add example metadata [`#132`](https://github.com/oceanprotocol/ocean-cli/pull/132) +- chore: buy v5 [`bc2b222`](https://github.com/oceanprotocol/ocean-cli/commit/bc2b2227c08620a63e171ab9e8d1660a3190bd4a) +- fix: removed file downaloded [`c99c066`](https://github.com/oceanprotocol/ocean-cli/commit/c99c06639dd6ad3a4c2f93ba01f5163bc77ec24f) +- fix: lint [`a91ee21`](https://github.com/oceanprotocol/ocean-cli/commit/a91ee21b6c59b44c3b93cee84286abf881bac1bc) + +#### v1.0.0 + +> 27 February 2026 + +- use oceanlib v6 [`#146`](https://github.com/oceanprotocol/ocean-cli/pull/146) +- feat(logs): add downloadNodeLogs cli command [`#144`](https://github.com/oceanprotocol/ocean-cli/pull/144) +- fix: update get logs test after getLogsHandler p2p update [`#143`](https://github.com/oceanprotocol/ocean-cli/pull/143) +- Feature/bump_oceanlib_to_510 [`#141`](https://github.com/oceanprotocol/ocean-cli/pull/141) +- add copilot instructions [`#140`](https://github.com/oceanprotocol/ocean-cli/pull/140) +- add n8n flow [`#139`](https://github.com/oceanprotocol/ocean-cli/pull/139) +- Node version and module type [`#137`](https://github.com/oceanprotocol/ocean-cli/pull/137) +- bump ocean libs [`#136`](https://github.com/oceanprotocol/ocean-cli/pull/136) +- Updated docs [`#135`](https://github.com/oceanprotocol/ocean-cli/pull/135) +- Access list methods [`#133`](https://github.com/oceanprotocol/ocean-cli/pull/133) +- update codeowners [`#134`](https://github.com/oceanprotocol/ocean-cli/pull/134) +- Update libs and ethers [`#127`](https://github.com/oceanprotocol/ocean-cli/pull/127) +- Fix publisher trusted algorithms structure for compute dataset. [`#125`](https://github.com/oceanprotocol/ocean-cli/pull/125) +- Feature/paid auth extra commands [`#124`](https://github.com/oceanprotocol/ocean-cli/pull/124) +- Fix parsing compute envs for ocean-node system tests. [`#121`](https://github.com/oceanprotocol/ocean-cli/pull/121) +- Feature/paid-compute in cli [`#115`](https://github.com/oceanprotocol/ocean-cli/pull/115) +- bump ocean.js [`#120`](https://github.com/oceanprotocol/ocean-cli/pull/120) +- Update ocean.js [`#119`](https://github.com/oceanprotocol/ocean-cli/pull/119) +- Remove legacy env vars like AQUARIUS and PROVIDER urls [`#111`](https://github.com/oceanprotocol/ocean-cli/pull/111) +- Update CODEOWNERS [`#112`](https://github.com/oceanprotocol/ocean-cli/pull/112) +- avoid cli to exit, loop commands [`#99`](https://github.com/oceanprotocol/ocean-cli/pull/99) +- add authorized publishers, update DDO validate call [`#90`](https://github.com/oceanprotocol/ocean-cli/pull/90) +- Fix start free compute - Fix logic for METADATA_CACHE_URI [`#109`](https://github.com/oceanprotocol/ocean-cli/pull/109) +- Release SDK 4.0 C2D V2 [`#89`](https://github.com/oceanprotocol/ocean-cli/pull/89) +- Issue ddo id with lib [`#101`](https://github.com/oceanprotocol/ocean-cli/pull/101) +- fix node branch [`#103`](https://github.com/oceanprotocol/ocean-cli/pull/103) +- use latest ocean.js [`#100`](https://github.com/oceanprotocol/ocean-cli/pull/100) +- Fix consume flow tests for new DDO structure [`#87`](https://github.com/oceanprotocol/ocean-cli/pull/87) +- Fix p2p test. [`#98`](https://github.com/oceanprotocol/ocean-cli/pull/98) +- Issue 96 consumeflow file [`#97`](https://github.com/oceanprotocol/ocean-cli/pull/97) +- updating cli to use command cli library [`#88`](https://github.com/oceanprotocol/ocean-cli/pull/88) +- Use createAsset function from SDK. [`#73`](https://github.com/oceanprotocol/ocean-cli/pull/73) +- pass config argument when we have it [`#84`](https://github.com/oceanprotocol/ocean-cli/pull/84) +- Fixing bug where feeToken is always Ocean [`#79`](https://github.com/oceanprotocol/ocean-cli/pull/79) +- refactor fn call waitForAqua to waitForIndexer [`#86`](https://github.com/oceanprotocol/ocean-cli/pull/86) +- increase MAX_REQ_PER_MINUTE for system tests [`#81`](https://github.com/oceanprotocol/ocean-cli/pull/81) +- Delete node:16 docker image from workflow. [`#76`](https://github.com/oceanprotocol/ocean-cli/pull/76) +- Setting up cli interactive flow [`#71`](https://github.com/oceanprotocol/ocean-cli/pull/71) +- add check for computeoutput, ips [`#65`](https://github.com/oceanprotocol/ocean-cli/pull/65) +- Adding tests for downloading assets [`#68`](https://github.com/oceanprotocol/ocean-cli/pull/68) +- Issue 62 - add default filename on downloadFile [`#63`](https://github.com/oceanprotocol/ocean-cli/pull/63) +- Updating access [`#67`](https://github.com/oceanprotocol/ocean-cli/pull/67) +- Fix compute assets samples [`#61`](https://github.com/oceanprotocol/ocean-cli/pull/61) +- add missing algo meta to request to start compute [`#59`](https://github.com/oceanprotocol/ocean-cli/pull/59) +- Issue 54 agreementid stop [`#56`](https://github.com/oceanprotocol/ocean-cli/pull/56) +- computeStatus - add support for `agreementId` [`#53`](https://github.com/oceanprotocol/ocean-cli/pull/53) +- Bump ocean.js lib [`#55`](https://github.com/oceanprotocol/ocean-cli/pull/55) +- Test flow with Ocean Node [`#49`](https://github.com/oceanprotocol/ocean-cli/pull/49) +- Bump ocean lib [`#51`](https://github.com/oceanprotocol/ocean-cli/pull/51) +- Compute jobs with multiple datasets & selectable compute env [`#34`](https://github.com/oceanprotocol/ocean-cli/pull/34) +- Fix/provider url override if env var is set [`#47`](https://github.com/oceanprotocol/ocean-cli/pull/47) +- fix for loop [`#48`](https://github.com/oceanprotocol/ocean-cli/pull/48) +- Fix validation order [`#45`](https://github.com/oceanprotocol/ocean-cli/pull/45) +- set proper flags for metadata [`#41`](https://github.com/oceanprotocol/ocean-cli/pull/41) +- fix editAsset command [`#44`](https://github.com/oceanprotocol/ocean-cli/pull/44) +- bump ocean.js [`#40`](https://github.com/oceanprotocol/ocean-cli/pull/40) +- Bump ocean lib and remove logs [`#39`](https://github.com/oceanprotocol/ocean-cli/pull/39) +- Fix/ unencrypt publish [`#38`](https://github.com/oceanprotocol/ocean-cli/pull/38) +- Publish DDO without encrypt option [`#35`](https://github.com/oceanprotocol/ocean-cli/pull/35) +- Fix readme private key [`#31`](https://github.com/oceanprotocol/ocean-cli/pull/31) +- Update README.md [`#30`](https://github.com/oceanprotocol/ocean-cli/pull/30) +- Fix/ Mnemonic - Private key & Provider - Aquarius usage [`#29`](https://github.com/oceanprotocol/ocean-cli/pull/29) +- Readme, package.json updates & small fixes [`#21`](https://github.com/oceanprotocol/ocean-cli/pull/21) +- Add download c2d Job Result [`#17`](https://github.com/oceanprotocol/ocean-cli/pull/17) +- Build-in support for Barge on macOS [`#18`](https://github.com/oceanprotocol/ocean-cli/pull/18) +- bump ocean.js lib [`#20`](https://github.com/oceanprotocol/ocean-cli/pull/20) +- Bump ocean.js lib 3.1.1 [`#16`](https://github.com/oceanprotocol/ocean-cli/pull/16) +- Feature/ Support oceanjs v3 [`#6`](https://github.com/oceanprotocol/ocean-cli/pull/6) +- Update URL [`#5`](https://github.com/oceanprotocol/ocean-cli/pull/5) +- remove old code [`719a969`](https://github.com/oceanprotocol/ocean-cli/commit/719a9694749b9f77f45a8bc66ed862f36cfc90c7) +- refactor vars [`3951f5f`](https://github.com/oceanprotocol/ocean-cli/commit/3951f5fa0826ae0d05bd1d25726d1e053fab28c1) +- minor fix, allow empty datasets [`0e0281c`](https://github.com/oceanprotocol/ocean-cli/commit/0e0281ce790045bc8b2a87b2b65e9de27646b245) diff --git a/CLAUDE.md b/CLAUDE.md index c3d7724..7091aac 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this project is -`ocean-cli` (package name `ocean-cli`, version 2.0.0) is a TypeScript CLI that wraps the Ocean Protocol JavaScript library (`@oceanprotocol/lib`, a.k.a. ocean.js) to publish, edit, consume/download, and run compute-to-data (C2D) on assets, plus manage escrow payments, access lists, persistent-storage buckets, auth tokens, and admin node logs. It talks to an **Ocean Node** (the single service that replaced the old standalone Provider and Aquarius apps — it does metadata caching, indexing, encryption, ordering, and compute) and to an EVM chain via an RPC endpoint. +`ocean-cli` (npm package `@oceanprotocol/cli`, version 2.0.0; installs a `bin` named `ocean-cli`) is a TypeScript CLI that wraps the Ocean Protocol JavaScript library (`@oceanprotocol/lib`, a.k.a. ocean.js) to publish, edit, consume/download, and run compute-to-data (C2D) on assets, plus manage escrow payments, access lists, persistent-storage buckets, auth tokens, and admin node logs. It talks to an **Ocean Node** (the single service that replaced the old standalone Provider and Aquarius apps — it does metadata caching, indexing, encryption, ordering, and compute) and to an EVM chain via an RPC endpoint. The package is pure ESM (`"type": "module"` in `package.json`). All relative imports MUST carry an explicit `.js` extension even though the source is `.ts` (e.g. `import { Commands } from "./commands.js"`). Node 22 is expected (`.nvmrc` = `22`; CI uses `22.5.1`). @@ -22,6 +22,7 @@ Scripts (from `package.json`): - `npm run test` — `npm run lint && npm run test:system` (lint is part of "test"). - `npm run test:system` — `npm run mocha 'test/**/*.test.ts'`. - `npm run mocha` — `NODE_OPTIONS='--experimental-require-module' mocha --config=test/.mocharc.json --node-env=test --exit`. +- `npm run release` — `release-it --non-interactive`: bumps version, builds, regenerates the changelog (`npm run changelog` = `auto-changelog -p`), commits, tags `v${version}`, pushes, and cuts a GitHub Release. Does **not** publish to npm (`release-it` config `npm.publish: false`) — pushing the tag triggers `.github/workflows/publish.yml`, which runs `npm publish` (`--tag next` for tags containing `next`, else `latest`). Mirrors `@oceanprotocol/lib`'s release flow. Mocha config (`test/.mocharc.json`): loader `ts-node/esm`, `bail: true` (stops at first failure), `timeout: 20000`, `exit: true`. @@ -50,7 +51,9 @@ npm run cli h # list commands ("h" and "help" are aliases npm run cli publish metadata/simpleDownloadDataset.json ``` -Important behavior of the entry point (`src/index.ts`): after running the command passed on argv **once**, the process enters an interactive REPL loop, printing `Enter command ('exit' | 'quit' or CTRL-C to terminate')` and reading further commands from stdin until you type `exit`/`quit`/`\q`. To get one-shot behavior (run and exit — required for CI and scripting) set `AVOID_LOOP_RUN=true`. In the REPL you may type either the bare command (`publish metadata/x.json`) or the full `npm run cli publish metadata/x.json` form. +`npm run cli` (= `npx tsx src/index.ts`) runs from source, no build. A globally installed copy (`npm i -g @oceanprotocol/cli`) exposes the same thing as the `ocean-cli` binary — `ocean-cli ` is equivalent to `npm run cli `. + +Important behavior of the entry point (`src/index.ts`): after running the command passed on argv **once**, the process enters an interactive REPL loop, printing `Enter command ('exit' | 'quit' | ESC or CTRL-C to terminate')` and reading further commands from stdin until you type `exit`/`quit`/`\q`, press **ESC** (TTY only), or hit CTRL-C. To get one-shot behavior (run and exit — required for CI and scripting) set `AVOID_LOOP_RUN=true`. In the REPL you may type either the bare command (`publish metadata/x.json`) or the full `npm run cli publish metadata/x.json` / `ocean-cli publish metadata/x.json` form (the leading prefix is stripped). Note: a pure help/version invocation (`--help`, `-h`, `--version`, `-V`, `h`, `help`) skips env-var validation and P2P bootstrap, so it works with no configuration; every other command still validates the env vars below. ### Required environment variables @@ -58,14 +61,15 @@ Validated at startup in `createCLI()` (`src/cli.ts`), which `process.exit(1)`s w - `PRIVATE_KEY` **or** `MNEMONIC` — signer credentials (private key preferred; mnemonic via `ethers.Wallet.fromPhrase`). - `RPC` — JSON-RPC endpoint; chainId is read from `provider.getNetwork()`, not configured manually. -- `NODE_URL` — the Ocean Node. Can be an `http(s)://` URL, a raw libp2p peer id, or a full `/dns4/.../p2p/...` multiaddr (triggers P2P mode, see below). ### Optional environment variables +- `NODE_URL` — the **initial** Ocean Node. An `http(s)://` URL, a raw libp2p peer id, or a full `/dns4/.../p2p/...` multiaddr. **Not required to start:** without it the CLI runs in a node-less state where the `preAction` gate in `createCLI()` refuses every command except `setNode` / `getNode` / `help` (see "Node selection"). Switchable at runtime with `setNode`. +- `DISABLE_P2P` — `true` skips starting libp2p entirely. Combined with a P2P `NODE_URL` it is a fatal contradiction (`exit(1)` at startup). - `ADDRESS_FILE` — path to a contracts `address.json`. Defaults to `${homedir}/.ocean/ocean-contracts/artifacts/address.json`. Needed by escrow / mint / access-list commands (see "Config & chain selection"). - `INDEXING_MAX_RETRIES` / `INDEXING_RETRY_INTERVAL` — how long to wait for an asset to be indexed. **Code defaults are 120 retries × 4000 ms** (`getIndexingWaitSettings()` in `helpers.ts`); the README's "100 / 3000" figures are stale. - `AVOID_LOOP_RUN` — `true` = one-shot (no REPL loop). Unset/`false` = interactive loop. -- `BOOTSTRAP_PEERS` — comma-separated extra libp2p multiaddrs, only used in P2P mode. +- `BOOTSTRAP_PEERS` — comma-separated extra libp2p multiaddrs, added to the bootstrap list built in `nodeConnection.ts`. ## CLI commands exposed @@ -78,6 +82,7 @@ All registered in `src/cli.ts` via Commander (`commander` v13). Every command su - Access lists: `createAccessList`, `addToAccessList`, `checkAccessList`, `removeFromAccessList`. - Persistent storage buckets: `createBucket`, `addFileToBucket`, `listBuckets`, `listFilesInBucket`, `getFileObject`, `deleteFile`. - Admin: `downloadNodeLogs`. +- Node selection: `setNode` (alias `useNode`), `getNode` (alias `currentNode`). - `help` / `h`. Per-command flags and examples are exhaustively documented in `README.md` ("Command Usage" / "Available Named Options Per Command"). A few load-bearing notes: @@ -90,9 +95,9 @@ Per-command flags and examples are exhaustively documented in `README.md` ("Comm ### Entry point and dispatch (`src/index.ts` → `src/cli.ts`) -`main()` in `index.ts` calls `createCLI()` (in `cli.ts`) to build the Commander `program`, records supported command names/aliases, prints the REPL banner, runs the initial argv command once, then loops on stdin (`waitForCommands`) unless `AVOID_LOOP_RUN=true`. It uses `program.exitOverride()` so Commander errors don't kill the loop. +`main()` in `index.ts` calls `createCLI()` (in `cli.ts`) to build the Commander `program`, records supported command names/aliases, prints the REPL banner, runs the initial argv command once, then loops on stdin (`runLoop`) unless `AVOID_LOOP_RUN=true`. It uses `program.exitOverride()` so Commander errors don't kill the loop. -`createCLI()` does three things: (1) validates the required env vars, (2) if `NODE_URL` is a P2P URI, sets up libp2p (see "Transport"), (3) registers every command. Each command's `.action(...)`: +`createCLI()` does four things: (1) validates `PRIVATE_KEY`/`MNEMONIC` and `RPC` — **unless** the invocation is a pure help/version one (`--help`/`-h`/`--version`/`-V`/`h`/`help`), which is detected from `process.argv` and skips both validation and P2P so those work with no config, (2) starts libp2p and health-checks `NODE_URL` if set (see "Transport" and "Node selection"), (3) registers the `preAction` gate that refuses non-`NODE_FREE_COMMANDS` while no node is set, (4) registers every command. Each command's `.action(...)`: 1. merges positional + option values, 2. calls the local `initializeSigner()` — builds a `JsonRpcProvider(RPC)`, a `Wallet` from `PRIVATE_KEY` (or `Wallet.fromPhrase(MNEMONIC)`), and reads `chainId` from the network, @@ -121,7 +126,7 @@ One big class holding all command logic. The constructor: `helpers.ts` is the seam between the CLI and ocean.js: -- `createAssetUtil(...)` wraps ocean.js `createAsset` (used by publish/publishAlgo and the interactive publisher). It resolves the active ERC20 template (`calculateActiveTemplateIndex` reads `@oceanprotocol/contracts` `ERC20Template.json` ABI from `node_modules`), and for **Oasis Sapphire** (`config.sdk === 'oasis'`) wraps the signer with `@oasisprotocol/sapphire-paratime` (`getSignerAccordingSdk`) and deploys an allow access list before creating the asset. +- `createAssetUtil(...)` wraps ocean.js `createAsset` (used by publish/publishAlgo and the interactive publisher). It resolves the active ERC20 template (`calculateActiveTemplateIndex` reads and `JSON.parse`s the `@oceanprotocol/contracts` `ERC20Template.json` ABI, resolved via `createRequire`/`require.resolve` so it works from any cwd — e.g. a global install — not a cwd-relative `node_modules` path), and for **Oasis Sapphire** (`config.sdk === 'oasis'`) wraps the signer with `@oasisprotocol/sapphire-paratime` (`getSignerAccordingSdk`) and deploys an allow access list before creating the asset. - `updateAssetMetadata(...)` — used by `editAsset`, `allowAlgo`, `disallowAlgo` and the interactive publisher. It validates the DDO via `aquarius.validate`, then either `ProviderInstance.encrypt`s the DDO (flags = 2) or hexlifies raw JSON (flags = 0) depending on the `encryptDDO` flag, then calls `nft.setMetadata`. - `handleComputeOrder(...)` — the ordering state machine used in compute: validOrder + no fees → reuse as-is; validOrder + fees → `datatoken.reuseOrder` paying only provider fees; no order → `orderAsset` (pay 1 datatoken + fees). Approves provider-fee tokens first when the fee amount > 0. - `resolveComputeInputs(...)` + `parseComputeInput(...)` — parse the datasets/algo CLI strings (DID | JSON object | array | mixed | legacy `[did:a,did:b]`), resolve DID entries through `aquarius.waitForIndexer`, pass raw `fileObject` entries through (aligned with a `null` DDO slot), and pick `providerURI` from the first DID-based DDO's `serviceEndpoint` (else fall back to `NODE_URL`). @@ -154,9 +159,20 @@ The `startCompute` **action in `cli.ts`** orchestrates a two-phase flow (not a s - **Auth tokens**: `generateAuthToken` / `invalidateAuthToken` via `ProviderInstance`. - **downloadNodeLogs** (admin): time-range or `--last N` hours; writes `/logs.json`. -### Transport: HTTP vs P2P +### Transport: HTTP vs P2P, and node selection (`src/nodeConnection.ts`) + +All node lifecycle logic lives in `nodeConnection.ts`; `cli.ts` only calls into it. + +**libp2p is transport, not a connection to one node.** Every ocean.js P2P call takes a `nodeUri` and dials that peer on demand (direct dial for a full multiaddr, DHT lookup for a bare peer id), so one libp2p node serves any number of Ocean nodes and switching between them never restarts or stops it. + +- `startP2P(initialNodeUrl?)` — called **once at startup**, not lazily and not only for P2P `NODE_URL`s, because bootstrap dials + DHT warm-up take seconds and should overlap with the user reading the prompt. The one exception is one-shot mode (`AVOID_LOOP_RUN="true"`), where `cli.ts` skips it — there is no later command to warm up for (see the exit note at the end of this section). It is **deliberately not awaited**; the stored promise swallows its own rejection (an unhandled rejection on a fire-and-forget promise would kill the process) and remembers the failure for `ensureP2PReady()`. No-op when `DISABLE_P2P=true` or when `ProviderInstance.getLibp2pNode()` is already non-null. Bootstrap peers = the initial node if it is a P2P URI (bare peer ids get the `/ip4/127.0.0.1/tcp/9001/ws/p2p/` localhost convention) + `BOOTSTRAP_PEERS` + four hard-coded Ocean bootstrap nodes (passing `bootstrapPeers` **replaces** the lib's defaults, so they must be listed explicitly). +- `ensureP2PReady()` — awaited by every P2P-bound path; throws a clear reason instead of hanging when P2P is unavailable. +- `validateNode(url)` — non-destructive health check via `ProviderInstance.getNodeStatus` under an `AbortSignal.timeout` (10 s HTTP, 30 s P2P since a bare peer id may need a DHT lookup). Over P2P the on-demand dial *is* the reachability check, which is what the old 20 s wait-for-target-peer polling loop did — that loop is gone. +- `getCurrentNodeUrl()` / `setCurrentNodeUrl()` / `hasNode()` — `process.env.NODE_URL` stays the **single source of truth**, so switching node is just mutating it: `Commands`' constructor and `getMetadataURI()` re-read it per use. + +`setNode` validates first and only then mutates the env var, so a failed switch leaves everything untouched — there is nothing to roll back. The switch itself never touches the RPC/signer (node selection is independent of them and must work when the RPC is slow); `setNode` calls `initializeSigner()` only *after* committing, under a 5 s `Promise.race` timeout, purely to warn when the node does not serve the RPC's chain. CI exercises both transports (matrix `[http, p2p]`). -If `NODE_URL` passes `isP2pUri()`, `createCLI()` boots a libp2p node via `ProviderInstance.setupP2P`, seeding bootstrap peers = local peer (derived from the peer id or full multiaddr) + `BOOTSTRAP_PEERS` + four hard-coded Ocean bootstrap nodes, and then **waits up to 20 s for the specific target peer** (from `NODE_URL`) to connect before proceeding, because signed commands fail if only bootstrap peers are connected. CI exercises both transports (matrix `[http, p2p]`). +**libp2p keeps the process alive.** A started libp2p node holds the event loop open, and even a clean `stop()` leaves a `MessagePort` behind (confirmed with `process.getActiveResourcesInfo()`). So `index.ts` ends with `if (await stopP2P()) { await flushOutput(); process.exit(...) }` — the flush matters because a piped stdout can still hold buffered output that `process.exit()` would discard. For the same reason the eager `startP2P` is **skipped in one-shot mode** (`AVOID_LOOP_RUN=true`): a one-shot run has no later command to warm up for, and would only pay startup + shutdown cost. One-shot runs that target a P2P node still get libp2p on demand via `validateNode` → `ensureP2PReady`. ### Interactive publish wizard (currently unwired) @@ -187,3 +203,5 @@ CI (`.github/workflows/ci.yml`) has three jobs: `build`, `lint`, and `test_syste - The 1-indexed vs 0-indexed args-array split between `Commands` methods is easy to get wrong when adding/renaming commands. - Running the CLI without `AVOID_LOOP_RUN=true` drops into a stdin REPL after the first command — surprising in scripts. - `fixAndParseProviderFees` is a regex JSON patcher for the initialize→start round trip; prefer fixing the data shape over extending the regex. +- A new command is refused when no node is set unless its canonical name is added to `NODE_FREE_COMMANDS` in `cli.ts`. The gate is a single root-level `preAction` hook keyed on `actionCommand.name()` (canonical, so aliases resolve for free) and throws a **plain `Error`, not a `CommanderError`** — that is what makes `index.ts` report it in red and keep the REPL alive, while one-shot mode exits 1. +- `supportedCommands` in `index.ts` uses `command.aliases()` (plural). The old `alias()` returned only the first alias, silently making extra aliases unreachable in the REPL. diff --git a/README.md b/README.md index 0015916..dda87c1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ With the Ocean CLI tool you can: - **Edit** existing assets. - **Consume** data services, ordering datatokens and downloading data. - **Compute to data** on public available datasets using a published algorithm. +- **Run on-demand services**: launch long-running containers (JupyterLab, inference servers, …) on compute environments, paid via escrow. - **Manage access control** with access lists for restricting dataset/algorithm access. - **Handle escrow payments** for compute jobs with deposit, withdrawal, and authorization. - **Manage authentication** with token generation and invalidation. @@ -33,13 +34,37 @@ If you run into problems, please open up a [new issue](https://github.com/oceanp ## 🏗 Installation & Usage -### Clone and install +### Install globally (recommended) + +Install the CLI from npm to get the `ocean-cli` command available everywhere: ```bash -$ git clone https://github.com/oceanprotocol/ocean-cli.git +npm install -g @oceanprotocol/cli +``` + +Then invoke it directly (from any directory): + +```bash +ocean-cli h # list commands +ocean-cli --version +ocean-cli publish metadata/simpleDownloadDataset.json +``` + +> `ocean-cli --help`, `ocean-cli -h`, `ocean-cli --version` and `ocean-cli h` work with **no** environment variables set. Every other command requires the env vars described below. + +### From source (for contributors) + +Clone and install, then run the CLI straight from TypeScript with `npm run cli` (no build step needed): + +```bash +git clone https://github.com/oceanprotocol/ocean-cli.git +cd ocean-cli npm install +npm run cli h ``` +> **The command examples in this README use the `npm run cli ` form. If you installed globally, drop the `npm run cli` prefix and use `ocean-cli ` instead — the two are otherwise identical.** In interactive mode you can paste either form; a leading `npm run cli` or `ocean-cli` token is stripped automatically. + ### Set up environment variables - Set a private key(by exporting env "PRIVATE_KEY") or a mnemonic (by exporting env "MNEMONIC") @@ -60,34 +85,50 @@ export MNEMONIC="XXXX" export RPC='XXXX' ``` -- Mandatory, Set an Ocean Node URL. Ocean Nodes infrastructure is responsible for handling assets indexing and metadata caching. It replaced old Provider and Aquarius standalone apps. +- Optional (but recommended), set an Ocean Node URL. Ocean Nodes infrastructure is responsible for handling assets indexing and metadata caching. It replaced old Provider and Aquarius standalone apps. ``` export NODE_URL='XXXX' ``` -- Optional, set ADDRESS_FILE if you want to use a custom set of smart contract address + `NODE_URL` is the **initial** node only. If it is not set the CLI still starts, but **only `setNode`, `getNode` and `help` are available** — every other command is refused with `No Ocean Node set` until you pick a node: +```bash +npm run cli # starts with no node +# > setNode http://127.0.0.1:8001 +# > getComputeEnvironments # now works ``` -export ADDRESS_FILE='path-to-address-file' + + You can switch node at any time with [`setNode`](#setnode) without restarting the CLI. See [`getNode`](#getnode) to check which node is active. + +- Optional, set DISABLE_P2P to `'true'` to skip starting the libp2p transport. In interactive mode the CLI starts libp2p at startup (in the background, so it does not delay the prompt) even when `NODE_URL` is an HTTP URL, so that a later switch to a P2P node does not have to wait for bootstrap peers and DHT warm-up. One-shot runs (`AVOID_LOOP_RUN='true'`) skip that warm-up — they have no later command to benefit from it — and start libp2p only when the node they target is a P2P one. Set this when you only ever use HTTP nodes and do not want the CLI dialing the public Ocean bootstrap nodes. + +```bash +export DISABLE_P2P='true' ``` -- Optional, set INDEXING_MAX_RETRIES to the max number of retries when waiting for an asset to be indexed. Default is 100 retries max. +- Optional, set ADDRESS_FILE if you want to use a custom set of smart contract address ``` -export INDEXING_MAX_RETRIES='100' +export ADDRESS_FILE='path-to-address-file' ``` -- Optional, set INDEXING_RETRY_INTERVAL to the interval (in miliseconds) for each retry when waiting for an asset to be indexed. Default is 3 seconds. +- Optional, set INDEXING_MAX_RETRIES to the max number of retries when waiting for an asset to be indexed. Default is 120 retries max. -``` -export INDEXING_RETRY_INTERVAL='3000' +```bash +export INDEXING_MAX_RETRIES='120' ``` -- Optional, set AVOID_LOOP_RUN to 'true' to run each command and exit afterwards (usefull for CI test env and default behaviour). IF not set or set to 'false' the CLI will listen interactively for commands, until exit is manually forced +- Optional, set INDEXING_RETRY_INTERVAL to the interval (in milliseconds) for each retry when waiting for an asset to be indexed. Default is 4 seconds (4000 ms). +```bash +export INDEXING_RETRY_INTERVAL='4000' ``` -export AVOID_LOOP_RUN='true/false' + +- Optional, set AVOID_LOOP_RUN to `'true'` to run a single command and exit afterwards (one-shot mode — required for CI and scripting). **By default the CLI is interactive**: it runs the command you pass (if any), then keeps reading further commands from a prompt, just like a REPL. Exit the interactive loop with `exit` / `quit`, the **ESC** key, or **CTRL-C**. + +```bash +export AVOID_LOOP_RUN='true' # one-shot; unset or 'false' = interactive loop ``` - Optional, set SSI_WALLET_API, SSI_WALLET_ID, SSI_WALLET_DID to support v5 DDOs (assets using credentialSubject and SSI policy flows). @@ -137,13 +178,42 @@ npm run cli [options] #### Help Commands - **General help:** - `npm run cli --help` or `npm run cli -h` + `npm run cli --help` or `npm run cli -h` (globally: `ocean-cli --help` / `ocean-cli -h`) + +- **Version:** + `npm run cli --version` (globally: `ocean-cli --version`) - **Command-specific help:** `npm run cli help ` #### Examples +**Choosing the Ocean Node:** + + + +- **Switch node (works inside the interactive loop, no restart needed):** + `npm run cli setNode http://127.0.0.1:8001` + Also accepts a peer id or a full multiaddr, and `--node`: + `npm run cli setNode --node /dns4/node.example/tcp/9001/ws/p2p/16Uiu2HAm...` + Alias: `useNode`. + + The node is health-checked before the switch: if it cannot be reached, the current node is kept and nothing changes. + + + +- **Show the node in use:** + `npm run cli getNode` (alias `currentNode`) — prints the active node plus its version and the chain(s) it serves. + +Notes when switching nodes: + +- **Compute jobs live on the node that started them.** After a switch, `getJobStatus` / `downloadJobResults` query the *new* node — switch back to look up older jobs. +- **For a node on your own machine, prefer the full multiaddr** (`/ip4/127.0.0.1/tcp/9001/ws/p2p/`) over a bare peer id: a bare id has to be found via DHT, which may not advertise localhost addresses. +- **In one-shot mode** (`AVOID_LOOP_RUN='true'`) `setNode` only validates the node and prints the result — the switch dies with the process. Use `NODE_URL` for one-shot runs. +- `chainId` still comes from `RPC`, never from the node. `setNode` warns when the node does not serve the chain your RPC is on. + +--- + **Get DDO:** - **Positional:** @@ -307,6 +377,78 @@ Instead of a DID, you can pass a full `ComputeAsset` (datasets) or `ComputeAlgor --- +### Service-on-Demand (long-running containers) + +Launch a long-running container (JupyterLab, an inference server, nginx, …) on a +compute environment. Unlike a compute job — which runs an algorithm to completion +and exits — an on-demand service stays up until it **expires**, is **stopped**, or +is **extended**, and is reachable through a forwarded port (`http://:`). + +Full happy path: + +```bash +# 1. Inspect the node's templates and which environments can run them +npm run cli getServiceTemplates + +# 2. Fund escrow and authorize the environment's consumer address as payee +# (maxLockSeconds must be at least duration + 3600) +npm run cli depositEscrow 0x 100 +npm run cli authorizeEscrow 0x 0x 100 90000 100 + +# 3a. Start from an operator template +npm run cli -- startService 3600 0x --template jupyter-cpu \ + --user-data '{"JUPYTER_TOKEN":"secret"}' + +# 3b. …or bring your own image (pass the tag via --tag, NOT inside --image) +npm run cli -- startService 3600 0x \ + --image nginxinc/nginx-unprivileged --tag alpine --ports 8080 + +# 4. Inspect / manage +npm run cli getServiceStatus # YOUR services, full detail +npm run cli -- getServices --status 40 # ALL owners' running services on the node (SERVICES_LIST) +npm run cli -- serviceLogs --since 10m +npm run cli -- extendService 1800 --accept true +npm run cli -- restartService # REUSE: bounce container unchanged +npm run cli -- restartService --cmd '["python","app.py"]' # optional cmd/entrypoint override +npm run cli -- restartService --image myrepo/algo --tag v2 # RESPEC: rebuild on a new image (#2119) +npm run cli stopService +``` + +Notes: + +- **Duration is in seconds.** The CLI prints an **estimated** cost for the payment + prompt; the **authoritative** cost is computed by the node and shown as + `Node-computed cost:` right after start. +- **Start is asynchronous.** `startService` returns immediately with a `serviceId` + in status `Starting (10)`; the CLI then polls until `Running (40)` (unless + `--wait false`) and prints the endpoint URL. If polling is interrupted, resume + with `getServiceStatus `. +- **Escrow must be funded and authorized before start.** Escrow shortfalls do not + fail the HTTP call — they surface as the job ending in `Error`/`*Failed` — so the + CLI pre-verifies funds/authorization client-side and aborts early with the exact + remediation commands. +- **Image spec:** provide at most one of `--tag`, `--checksum`, `--dockerfile`, and + keep the tag in `--tag` (an image reference that already contains a tag makes the + node build an invalid `image:tag:latest` reference). +- **Ports:** containers run with `CapDrop ALL` and cannot bind ports below 1024 — + services must listen on a high container port (e.g. 8080). +- **`--user-data`** is a plain JSON object of container env vars; ocean.js encrypts + it to the node's key. The CLI **never logs its values** (keys only). +- **`getServiceStatus` vs `getServices`:** `getServiceStatus` shows *your* services + with full detail; `getServices` (alias `listServices`, the SERVICES_LIST command) + lists services across *all* owners on the node, with the docker image spec + stripped, and supports `--status` / `--include-all` / `--from` filters. +- **`restartService` has two modes (#2119):** with **no** container-spec flags the + container bounces on its stored spec (REUSE); supplying any image-spec flag + (`--image`, `--tag`, `--checksum`, `--dockerfile`, `--additional-docker-files`) + rebuilds the container on the new spec (RESPEC), keeping the same ports, expiry + and payment window at no extra charge. +- **`restartService --cmd/--entrypoint`** replace the stored command/entrypoint on + the recreated container (an empty array clears them); omit to reuse the stored + configuration. + +--- + **Mint Ocean:** - **Positional:** @@ -513,6 +655,12 @@ Instead of a DID, you can pass a full `ComputeAsset` (datasets) or `ComputeAlgor #### Available Named Options Per Command +- **setNode** (alias `useNode`)**:** + `` (Positional. HTTP(S) URL, peer id or full multiaddr) + `-n, --node ` (Same as the positional) + +- **getNode** (alias `currentNode`)**:** no arguments + - **getDDO:** `-d, --did ` @@ -581,6 +729,58 @@ Instead of a DID, you can pass a full `ComputeAsset` (datasets) or `ComputeAlgor `-i, --index ` `-f, --folder [destinationFolder]` +- **getServiceTemplates:** (alias `serviceTemplates`) + `[node]` (Optional positional. Ocean Node URL or peer id to query; defaults to `NODE_URL`) + `-n, --node ` (Optional. Same as the positional) + +- **startService:** + `` `` (seconds) `` (required positionals) + `--template ` (Start from an operator-published template) + `-i, --image ` (Container image — alternative to `--template`; keep the tag in `--tag`) + `--tag ` / `--checksum ` / `--dockerfile ` (image spec — provide at most one) + `--additional-docker-files ` (JSON file of `{filename: content}`, used with `--dockerfile`) + `--cmd ` / `--entrypoint ` (Docker CMD/ENTRYPOINT override as JSON arrays) + `-p, --ports ` (Comma-separated container ports, e.g. `8888,8080`) + `-r, --resources ` (Stringified JSON `[{"id":"cpu","amount":1},…]`; defaults to template requirements) + `-u, --user-data ` / `--user-data-file ` (Container env vars; encrypted to the node, never logged) + `--accept [boolean]` (Auto-confirm payment) + `--wait [boolean]` (Poll until Running/failure; default `true`) + `--timeout ` (Max seconds to wait for Running; default 600) + +- **getServiceStatus:** (alias `myServices`) + `[serviceId]` (Optional; omit to list all your services) + `-s, --service ` + `-v, --verbose [boolean]` (Dump full job objects) + +- **getServices:** (alias `listServices` — the SERVICES_LIST command, all owners) + `[node]` / `-n, --node ` (Optional Ocean Node URL or peer id; defaults to `NODE_URL`) + `--status ` (Filter by a single status number, e.g. `40` for Running) + `--include-all [boolean]` (Include all statuses, not just active reservations) + `--from ` (Only services created at/after this ISO string or Unix timestamp) + `-v, --verbose [boolean]` (Dump full job objects) + +- **serviceLogs:** (alias `computeServiceLogs`) + `` / `-s, --service ` + `--since ` (Unix seconds or a relative duration like `30s` / `2h`) + +- **extendService:** + `` `` (seconds) `[paymentToken]` + `-s, --service ` + `--duration ` + `-t, --token [paymentToken]` (defaults to the token used at start) + `--accept [boolean]` (Auto-confirm payment) + +- **restartService:** + `` + `-u, --user-data ` / `--user-data-file ` (REPLACE stored env vars) + `--cmd ` / `--entrypoint ` (REPLACE stored Docker CMD/ENTRYPOINT; empty array clears) + `--image ` / `--tag ` / `--checksum ` / `--dockerfile ` / `--additional-docker-files ` (RESPEC: rebuild the container on a new image spec; #2119) + `--wait [boolean]` (Poll until Running; default `true`) + `--timeout ` (Max seconds to wait; default 600) + +- **stopService:** + `` / `-s, --service ` + - **mintOcean:** No options/arguments required. diff --git a/package-lock.json b/package-lock.json index 13bf7dc..e341ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,30 @@ { - "name": "ocean-cli", - "version": "2.0.0", + "name": "@oceanprotocol/cli", + "version": "2.0.0-next.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ocean-cli", - "version": "2.0.0", + "name": "@oceanprotocol/cli", + "version": "2.0.0-next.4", "license": "Apache-2.0", "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.5.0", "@oceanprotocol/ddo-js": "^0.3.0", - "@oceanprotocol/lib": "^8.0.6", + "@oceanprotocol/lib": "^9.0.0-next.10", "axios": "^1.11.0", + "chalk": "^4.1.2", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", "decimal.js": "^10.4.1", "enquirer": "^2.4.1", - "esm": "^3.2.25", "ethers": "^6.15.0", - "figlet": "^1.7.0", - "ts-node": "^10.9.1", - "tsx": "^4.19.3" + "figlet": "^1.7.0" + }, + "bin": { + "ocean-cli": "dist/index.js" }, "devDependencies": { "@eslint/js": "^9.4.0", @@ -32,6 +33,7 @@ "@types/node": "^20.2.5", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", + "auto-changelog": "^2.4.0", "chai": "^4.3.7", "crypto": "^1.0.1", "eslint": "^8.44.0", @@ -43,9 +45,14 @@ "mocha": "^10.2.0", "prettier": "^2.8.8", "pretty-quick": "^3.1.3", + "release-it": "^19.2.4", + "ts-node": "^10.9.1", "tsx": "^4.19.2", "typescript": "^5.0.4", "typescript-eslint": "^7.12.0" + }, + "engines": { + "node": ">=22" } }, "node_modules/@adraffy/ens-normalize": { @@ -1848,6 +1855,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -1860,6 +1868,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2952,6 +2961,401 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -2967,6 +3371,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2987,6 +3392,7 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -3074,7 +3480,17 @@ "node": ">= 8" } }, - "node_modules/@oasisprotocol/deoxysii": { + "node_modules/@nodeutils/defaults-deep": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodeutils/defaults-deep/-/defaults-deep-1.1.0.tgz", + "integrity": "sha512-gG44cwQovaOFdSR02jR9IhVRpnDP64VN6JdjYJTfNz4J4fWn7TQnmrf22nSjRqlwlxPcW8PL/L3KbJg3tdwvpg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.15.0" + } + }, + "node_modules/@oasisprotocol/deoxysii": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@oasisprotocol/deoxysii/-/deoxysii-0.0.5.tgz", "integrity": "sha512-a6wYPjk8ALDIiQW/971AKOTSTY1qSdld+Y05F44gVZvlb3FOyHfgbIxXm7CZnUG1A+jK49g5SCWYP+V3/Tc75Q==", @@ -3185,33 +3601,220 @@ } }, "node_modules/@oceanprotocol/lib": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-8.1.0.tgz", - "integrity": "sha512-LDh7RLrBwBypacAA99+x3+Ti3eKs2FRptE+90S2KnIDqBTOgEoR6iQHjjw47AGm23YBgNF7f0W2vocfhYnfkqA==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-9.0.0-next.10.tgz", + "integrity": "sha512-WFRCuqAP13V3na4l0weQr2wqOKsRtFtVg9kNiJg9ia055bt93AtD9PRBtiSUltuOxAbxAiikYPyea8hy8z2zUw==", "license": "Apache-2.0", "dependencies": { "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/ddo-js": "^0.3.0", "bignumber.js": "^9.3.1", - "cross-fetch": "^4.0.0", "crypto-js": "^4.1.1", "decimal.js": "^10.4.1", "eciesjs": "^0.4.5", "ethers": "^6.15.0", - "form-data": "^2.3.3", "jsonwebtoken": "^9.0.2" }, + "engines": { + "node": ">=18" + }, "peerDependencies": { "web3": "^1.8.0" } }, - "node_modules/@oceanprotocol/lib/node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, "license": "MIT", "dependencies": { - "node-fetch": "^2.7.0" + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.11.tgz", + "integrity": "sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "content-type": "^2.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@phun-ky/typeof": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-2.0.3.tgz", + "integrity": "sha512-oeQJs1aa8Ghke8JIK9yuq/+KjMiaYeDZ38jx7MhkXncXlUKjqQ3wEm2X3qCKyjo+ZZofZj+WsEEiqkTtRuE2xQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.9.0 || >=22.0.0", + "npm": ">=10.8.2" + }, + "funding": { + "url": "https://github.com/phun-ky/typeof?sponsor=1" } }, "node_modules/@rdfjs/data-model": { @@ -3525,6 +4128,13 @@ "node": ">=14.16" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tpluscode/rdf-ns-builders": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tpluscode/rdf-ns-builders/-/rdf-ns-builders-4.3.0.tgz", @@ -3552,24 +4162,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, "license": "MIT" }, "node_modules/@types/bn.js": { @@ -3663,6 +4277,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -3974,6 +4595,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3996,6 +4618,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -4010,6 +4633,16 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4048,7 +4681,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4078,6 +4710,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -4294,6 +4927,19 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -4318,6 +4964,16 @@ "license": "MIT", "peer": true }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4331,6 +4987,49 @@ "dev": true, "license": "MIT" }, + "node_modules/auto-changelog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.6.0.tgz", + "integrity": "sha512-jJgUkuWXQ7fPLPXOMQk/XSSmy7KvzpzhjJa6w660dq1KTEez/GW2CPckBra3jMMMMpcwxp84bOslo29qRCewzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "handlebars": "^4.7.9", + "import-cwd": "^3.0.0", + "parse-github-url": "^1.0.4", + "semver": "^7.8.1" + }, + "bin": { + "auto-changelog": "src/index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/auto-changelog/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/auto-changelog/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -4553,6 +5252,16 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4570,6 +5279,13 @@ "license": "Unlicense", "peer": true }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -4880,6 +5596,22 @@ "semver": "^7.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4890,6 +5622,65 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", + "giget": "^2.0.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cacheable-lookup": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz", @@ -5094,7 +5885,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5107,6 +5897,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -5165,6 +5962,22 @@ "license": "ISC", "peer": true }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cids": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/cids/-/cids-0.7.5.tgz", @@ -5235,13 +6048,62 @@ "node": ">= 0.10" } }, - "node_modules/class-is": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", - "integrity": "sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==", + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-is": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", + "integrity": "sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==", "license": "MIT", "peer": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -5282,7 +6144,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5295,7 +6156,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colord": { @@ -5350,6 +6210,23 @@ "source-map": "^0.6.1" } }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5507,6 +6384,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, "license": "MIT" }, "node_modules/cross-fetch": { @@ -5904,6 +6782,36 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5959,6 +6867,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5978,6 +6908,13 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6090,6 +7027,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6574,6 +7524,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -7229,15 +8201,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -7272,6 +8235,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -7325,6 +8302,19 @@ "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.5.0.tgz", + "integrity": "sha512-qifAYjuW5AM1eEEIsFnOwB+TGqu6ynU3OKj9WbUTOtUBHFPZqL03XUW34kbp3zm19Ald+U8dEyRXaVsUck+Y1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -7726,6 +8716,13 @@ "license": "MIT", "peer": true }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -8095,23 +9092,6 @@ "node": "*" } }, - "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/form-data-encoder": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", @@ -8282,6 +9262,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -8329,6 +9322,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -8360,6 +9366,31 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -8370,6 +9401,45 @@ "assert-plus": "^1.0.0" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-up": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^9.2.0" + } + }, + "node_modules/git-url-parse": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^8.1.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8570,6 +9640,28 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -8635,7 +9727,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8802,6 +9893,20 @@ "license": "ISC", "peer": true }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -8832,6 +9937,20 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -9009,6 +10128,33 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -9024,6 +10170,16 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9306,14 +10462,62 @@ "npm": ">=3" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9443,6 +10647,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -9592,6 +10819,23 @@ "license": "MIT", "peer": true }, + "node_modules/issue-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -9643,6 +10887,16 @@ "node": ">= 10.13.0" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -9730,6 +10984,13 @@ "license": "ISC", "peer": true }, + "node_modules/json-with-bigint": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.10.tgz", + "integrity": "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10008,6 +11269,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -10015,6 +11283,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10022,6 +11297,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -10085,6 +11367,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -10150,6 +11439,19 @@ "node": ">=10" } }, + "node_modules/macos-release": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.5.1.tgz", + "integrity": "sha512-Lci/1in+elqZ589PXnfP/iwZXpwQifTM94WJRQwG2tZSdfY7NfB/aUaTARHrohWCgHlXoabeaeXRDOnF5X9JQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -10190,6 +11492,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, "node_modules/math-intrinsics": { @@ -10512,6 +11815,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -10841,6 +12157,16 @@ "buffer": "^5.5.0" } }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/n3": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", @@ -10904,6 +12230,39 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/new-github-release-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-2.0.0.tgz", + "integrity": "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.5.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -10958,6 +12317,13 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -11067,6 +12433,31 @@ "license": "MIT", "peer": true }, + "node_modules/nypm": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.8.tgz", + "integrity": "sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "dev": true, + "license": "MIT" + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -11208,6 +12599,13 @@ "http-https": "^1.0.0" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -11282,6 +12680,136 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/os-name": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz", + "integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -11392,6 +12920,40 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11405,6 +12967,19 @@ "node": ">=6" } }, + "node_modules/parse-github-url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.4.tgz", + "integrity": "sha512-CEtCOt55fHmd6DpBc/N7H5NC4vJpcquhzzs9Iw2mRj8bVxo1O5TQI5MXKOMO7+yBOqD+5dKCCRK4Kj1KskZc6Q==", + "dev": true, + "license": "MIT", + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-headers": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", @@ -11431,6 +13006,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" + }, + "engines": { + "node": ">=14.13.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11495,6 +13094,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -11557,6 +13163,13 @@ "inherits": "^2.0.1" } }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11666,6 +13279,18 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12475,6 +14100,13 @@ "react-is": "^16.13.1" } }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12489,6 +14121,43 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -12630,6 +14299,17 @@ "node": ">= 0.8" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/rdf-canonize": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", @@ -12899,6 +14579,133 @@ "node": ">=6" } }, + "node_modules/release-it": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.2.4.tgz", + "integrity": "sha512-BwaJwQYUIIAKuDYvpqQTSoy0U7zIy6cHyEjih/aNaFICphGahia4cjDANuFXb7gVZ51hIK9W0io6fjNQWXqICg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/webpro" + } + ], + "license": "MIT", + "dependencies": { + "@nodeutils/defaults-deep": "1.1.0", + "@octokit/rest": "22.0.1", + "@phun-ky/typeof": "2.0.3", + "async-retry": "1.3.3", + "c12": "3.3.3", + "ci-info": "^4.3.1", + "eta": "4.5.0", + "git-url-parse": "16.1.0", + "inquirer": "12.11.1", + "issue-parser": "7.0.1", + "lodash.merge": "4.6.2", + "mime-types": "3.0.2", + "new-github-release-url": "2.0.0", + "open": "10.2.0", + "ora": "9.0.0", + "os-name": "6.1.0", + "proxy-agent": "6.5.0", + "semver": "7.7.3", + "tinyglobby": "0.2.15", + "undici": "6.23.0", + "url-join": "5.0.0", + "wildcard-match": "5.1.4", + "yargs-parser": "21.1.1" + }, + "bin": { + "release-it": "bin/release-it.js" + }, + "engines": { + "node": "^20.12.0 || >=22.0.0" + } + }, + "node_modules/release-it/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-it/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/release-it/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/release-it/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-it/node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/release-it/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -13038,6 +14845,62 @@ "node": ">=8" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -13394,7 +15257,30 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } }, "node_modules/run-parallel": { "version": "1.2.0", @@ -13420,6 +15306,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -13528,8 +15424,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/scrypt-js": { "version": "3.0.1", @@ -13562,9 +15457,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13908,6 +15803,47 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13998,6 +15934,19 @@ "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -14238,7 +16187,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -14545,6 +16493,64 @@ "globrex": "^0.1.2" } }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -14620,6 +16626,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -14663,6 +16670,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -14909,6 +16917,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -15164,6 +17173,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uint32": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/uint32/-/uint32-0.2.1.tgz", @@ -15258,6 +17281,13 @@ "node": ">=4" } }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -15318,6 +17348,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/url-set-query": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz", @@ -15391,6 +17431,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, "license": "MIT" }, "node_modules/varint": { @@ -16040,6 +18081,147 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "dev": true, + "license": "ISC" + }, + "node_modules/windows-release": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/windows-release/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/windows-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/windows-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -16050,6 +18232,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", @@ -16102,6 +18291,38 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xhr": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", @@ -16237,6 +18458,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -16254,6 +18476,32 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index f848b59..033bcd8 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,16 @@ { - "name": "ocean-cli", - "version": "2.0.0", + "name": "@oceanprotocol/cli", + "version": "2.0.0-next.4", "description": "CLI tool to interact with the oceanprotocol's JavaScript library to privately & securely publish, consume and run compute on data.", "type": "module", + "bin": { + "ocean-cli": "dist/index.js" + }, + "files": [ + "dist", + "metadata", + "README.md" + ], "scripts": { "build": "npm run clean && npm run build:tsc", "build:tsc": "tsc --sourceMap", @@ -13,17 +21,42 @@ "cli": "npx tsx src/index.ts", "test:system": "npm run mocha 'test/**/*.test.ts'", "test": "npm run lint && npm run test:system", - "mocha": "NODE_OPTIONS='--experimental-require-module' mocha --config=test/.mocharc.json --node-env=test --exit" + "mocha": "NODE_OPTIONS='--experimental-require-module' mocha --config=test/.mocharc.json --node-env=test --exit", + "prepublishOnly": "npm run build", + "changelog": "auto-changelog -p", + "release": "release-it --non-interactive" }, "author": "Ocean Protocol ", "license": "Apache-2.0", "engines": { "node": ">=22" }, + "repository": { + "type": "git", + "url": "git+https://github.com/oceanprotocol/ocean-cli.git" + }, + "publishConfig": { + "access": "public" + }, "bugs": { - "url": "https://github.com/oceanprotocol/ocean.js-cli/issues" + "url": "https://github.com/oceanprotocol/ocean-cli/issues" + }, + "homepage": "https://github.com/oceanprotocol/ocean-cli#readme", + "release-it": { + "hooks": { + "after:bump": "npm run build && npm run changelog && git add CHANGELOG.md" + }, + "git": { + "tagName": "v${version}", + "commitMessage": "Release v${version}" + }, + "github": { + "release": true + }, + "npm": { + "publish": false + } }, - "homepage": "https://github.com/oceanprotocol/ocean.js-cli#readme", "devDependencies": { "@eslint/js": "^9.4.0", "@types/chai": "^4.3.5", @@ -31,6 +64,7 @@ "@types/node": "^20.2.5", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", + "auto-changelog": "^2.4.0", "chai": "^4.3.7", "crypto": "^1.0.1", "eslint": "^8.44.0", @@ -42,6 +76,8 @@ "mocha": "^10.2.0", "prettier": "^2.8.8", "pretty-quick": "^3.1.3", + "release-it": "^19.2.4", + "ts-node": "^10.9.1", "tsx": "^4.19.2", "typescript": "^5.0.4", "typescript-eslint": "^7.12.0" @@ -50,17 +86,15 @@ "@oasisprotocol/sapphire-paratime": "^1.3.2", "@oceanprotocol/contracts": "^2.5.0", "@oceanprotocol/ddo-js": "^0.3.0", - "@oceanprotocol/lib": "^8.0.6", + "@oceanprotocol/lib": "^9.0.0-next.10", "axios": "^1.11.0", + "chalk": "^4.1.2", "commander": "^13.1.0", "cross-fetch": "^3.1.5", "crypto-js": "^4.1.1", "decimal.js": "^10.4.1", "enquirer": "^2.4.1", - "esm": "^3.2.25", "ethers": "^6.15.0", - "figlet": "^1.7.0", - "ts-node": "^10.9.1", - "tsx": "^4.19.3" + "figlet": "^1.7.0" } } diff --git a/src/cli.ts b/src/cli.ts index 89d9878..faa1271 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,11 +1,66 @@ import { Command } from "commander"; import { Commands } from "./commands.js"; import { JsonRpcProvider, Signer, ethers } from "ethers"; +import fs from "fs"; +import { createRequire } from "module"; import chalk from "chalk"; import { stdin as input, stdout } from "node:process"; import { createInterface } from "readline/promises"; -import { unitsToAmount, ProviderInstance, isP2pUri } from "@oceanprotocol/lib"; +import { + unitsToAmount, + isP2pUri, + ServiceStatusNumber, + ServiceRestartParams, +} from "@oceanprotocol/lib"; import { toBoolean } from "./helpers.js"; +import { + getCurrentNodeUrl, + hasNode, + nodeChainIds, + setCurrentNodeUrl, + startP2P, + validateNode, +} from "./nodeConnection.js"; + +// Commands usable before any Ocean Node is selected. Everything else is refused by the +// preAction gate below until `setNode` succeeds. Canonical names only — aliases +// (useNode, currentNode, h) resolve to these. +const NODE_FREE_COMMANDS = new Set(["setNode", "getNode", "help"]); + +// Single source of truth for the CLI version: read it from package.json instead +// of hardcoding, so it can't drift. `../package.json` resolves from both src/ +// (dev via tsx) and dist/ (published), since both sit one level below the root. +const pkg = createRequire(import.meta.url)("../package.json"); + +// Parse a CLI JSON array-of-strings option (e.g. --cmd '["python","app.py"]'). +// Returns the array, or throws with a clear message for the action to surface. +function parseJsonStringArray(name: string, value: string): string[] { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new Error(`${name} must be a JSON array, e.g. '["a","b"]'`); + } + if (!Array.isArray(parsed) || !parsed.every((x) => typeof x === "string")) { + throw new Error(`${name} must be a JSON array of strings`); + } + return parsed as string[]; +} + +// Parse a comma-separated port list, validating each is an integer 1-65535. +function parsePorts(value: string): number[] { + return value + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + .map((p) => { + const n = parseInt(p, 10); + if (!Number.isInteger(n) || n < 1 || n > 65535) { + throw new Error(`Invalid port "${p}" (must be an integer 1-65535)`); + } + return n; + }); +} async function initializeSigner() { const provider = new JsonRpcProvider(process.env.RPC); @@ -22,97 +77,76 @@ async function initializeSigner() { } export async function createCLI() { - if (!process.env.MNEMONIC && !process.env.PRIVATE_KEY) { - console.error(chalk.red("Have you forgot to set MNEMONIC or PRIVATE_KEY?")); - process.exit(1); - } - if (!process.env.RPC) { - console.error(chalk.red("Have you forgot to set env RPC?")); - process.exit(1); - } + // A pure help/version invocation must work with no configuration at all (a + // globally installed binary is expected to answer `--help`/`--version` + // without credentials). Detect it from argv and skip both env validation and + // the P2P bootstrap in that case; every real command still validates below. + const argv = process.argv.slice(2); + const isHelpOrVersion = + argv.includes("--help") || + argv.includes("-h") || + argv.includes("--version") || + argv.includes("-V") || + argv[0] === "help" || + argv[0] === "h"; - if (!process.env.NODE_URL) { - console.error(chalk.red("Have you forgot to set env NODE_URL?")); - process.exit(1); + if (!isHelpOrVersion) { + if (!process.env.MNEMONIC && !process.env.PRIVATE_KEY) { + console.error(chalk.red("Have you forgot to set MNEMONIC or PRIVATE_KEY?")); + process.exit(1); + } + if (!process.env.RPC) { + console.error(chalk.red("Have you forgot to set env RPC?")); + process.exit(1); + } } - if (isP2pUri(process.env.NODE_URL)) { - const extra = process.env.BOOTSTRAP_PEERS?.split(",").filter(Boolean) || []; - - // Default Ocean bootstrap nodes (must be included explicitly since passing - // bootstrapPeers to setupP2P replaces the built-in defaults) - const oceanDefaults = [ - "/dns4/bootstrap1.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmLhRDqfufZiQnxvQs2XHhd6hwkLSPfjAQg1gH8wgRixiP", - "/dns4/bootstrap2.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmHwzeVw7RpGopjZe6qNBJbzDDBdqtrSk7Gcx1emYsfgL4", - "/dns4/bootstrap3.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmBKSeEP3v4tYEPsZsZv9VELinyMCsrVTJW9BvQeFXx28U", - "/dns4/bootstrap4.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmSTVTArioKm2wVcyeASHYEsnx2ZNq467Z4GMDU4ErEPom", - ]; - - const nodeUrl = process.env.NODE_URL; - const isFullMultiaddr = - nodeUrl.startsWith("/") && nodeUrl.includes("/p2p/"); - const localPeer = isFullMultiaddr - ? [nodeUrl] - : [`/ip4/127.0.0.1/tcp/9001/ws/p2p/${nodeUrl}`]; - const bootstrapPeers = [...localPeer, ...extra, ...oceanDefaults]; - console.log(chalk.cyan("P2P mode detected. Initializing libp2p...")); - console.log(chalk.cyan(`Bootstrap peers: ${bootstrapPeers.length}`)); - - for (const peer of localPeer) { - console.log(chalk.cyan(` Local: ${peer}`)); + // NODE_URL is optional: without it the CLI still starts, but only the commands in + // NODE_FREE_COMMANDS are accepted until `setNode` picks a node (see the gate below). + if (!isHelpOrVersion) { + if (process.env.DISABLE_P2P === "true" && isP2pUri(getCurrentNodeUrl())) { + console.error( + chalk.red( + "NODE_URL is a P2P URI but DISABLE_P2P=true — no command could reach it." + ) + ); + process.exit(1); } - // Allow localhost connections / local nodes - await ProviderInstance.setupP2P({ - bootstrapPeers, - libp2p: { - connectionGater: { - denyDialMultiaddr: () => false, - }, - }, - } as any); - console.log( - chalk.cyan("libp2p node started. Waiting for peer connections...") - ); - // Wait for the TARGET peer (the one in NODE_URL) to be connected, - // not just any bootstrap peer — otherwise signed commands fail with - // "Cannot reach peer ...". - const targetPeerId = isFullMultiaddr - ? nodeUrl.split("/p2p/").pop()! - : nodeUrl; - const maxWait = 20_000; - const interval = 500; - let waited = 0; - const libp2p = (ProviderInstance as any).p2pProvider?.libp2pNode; - const isTargetConnected = () => - (libp2p?.getPeers() ?? []).some( - (p: { toString(): string }) => p.toString() === targetPeerId - ); - while (waited < maxWait) { - if (isTargetConnected()) { - const total = libp2p?.getConnections()?.length ?? 0; + // Eager, non-blocking: libp2p warms up (bootstrap dials + DHT) while the user reads + // the prompt, so a later switch to a P2P node doesn't pay that cost interactively. + // + // Only in loop mode. A one-shot run has no "later command" to warm up for, and + // starting a libp2p node it never uses would just delay its exit — the process + // cannot end until the node is up and stopped again. One-shot runs that *do* target + // a P2P node still get it: validateNode() -> ensureP2PReady() starts it on demand. + if (process.env.AVOID_LOOP_RUN !== "true") { + startP2P(process.env.NODE_URL); + } + + if (hasNode()) { + // Confirms the startup node is reachable before the user types anything. For a + // P2P node the status call dials the peer, which is what the old wait-for-peer + // polling loop used to do. + const status = await validateNode(getCurrentNodeUrl()); + if (status) { console.log( chalk.green( - `Connected to target peer ${targetPeerId.slice(0, 12)}… in ${waited}ms (total peers: ${total})` + `Using node ${getCurrentNodeUrl()} (version ${status.version})` ) ); - break; - } - await new Promise((r) => setTimeout(r, interval)); - waited += interval; - if (waited % 3000 === 0) { - const total = libp2p?.getConnections()?.length ?? 0; - console.log( - chalk.yellow( - ` Waiting for target peer ${targetPeerId.slice(0, 12)}… (${waited / 1000}s, ${total} other peer(s))` + } else { + console.error( + chalk.red( + `Node ${getCurrentNodeUrl()} is not reachable. Commands may fail.` ) ); } - } - if (!isTargetConnected()) { - console.error( - chalk.red( - `Target peer ${targetPeerId} not reachable after ${maxWait / 1000}s. Commands will fail.` + } else { + console.log( + chalk.yellow( + "No Ocean Node configured. Run `setNode ` to choose one — " + + `only ${[...NODE_FREE_COMMANDS].join(", ")} are available until then.` ) ); } @@ -123,9 +157,26 @@ export async function createCLI() { program .name("ocean-cli") .description("CLI tool to interact with Ocean Protocol") - .version("2.0.0") + .version(pkg.version) .helpOption("-h, --help", "Display help for command"); + // Every command except the node-free ones needs an Ocean Node. A single preAction + // hook on the root program runs before *any* subcommand action, so this covers both + // the REPL loop and one-shot mode without touching 40 action bodies. + // + // A plain Error (not a CommanderError) is thrown on purpose: in loop mode index.ts + // reports it in red and keeps the REPL alive; in one-shot mode main() reports it and + // exits 1, so scripts see a non-zero status. + program.hook("preAction", (_thisCommand, actionCommand) => { + // actionCommand.name() is the canonical name, so aliases resolve for free. + if (!hasNode() && !NODE_FREE_COMMANDS.has(actionCommand.name())) { + throw new Error( + `No Ocean Node set. Run \`setNode \` first ` + + `(available now: ${[...NODE_FREE_COMMANDS].join(", ")}).` + ); + } + }); + // Custom help command to support legacy "h" invocation. // Note: We use console.log(program.helpInformation()) to print the full help output. program @@ -136,6 +187,96 @@ export async function createCLI() { console.log(program.helpInformation()); }); + // setNode command. The switch itself never touches the RPC/signer: choosing a node is + // independent of them and must work even when the RPC is slow or wrong. The RPC is + // consulted only afterwards, under a timeout, to warn about a chain mismatch. + program + .command("setNode") + .alias("useNode") + .description( + "Sets / switches the Ocean Node used by subsequent commands, without restarting" + ) + .argument("", "HTTP(S) URL, peer id or full multiaddr of the node") + .option("-n, --node ", "Ocean Node to use") + .action(async (nodeUrl, options) => { + const target = options.node || nodeUrl; + const previous = getCurrentNodeUrl(); + if (target === previous) { + console.log(chalk.green(`Node ${target} is already the active one.`)); + return; + } + + const status = await validateNode(target); + if (!status) { + console.error( + chalk.red( + previous + ? `Cannot reach ${target}. Keeping current node: ${previous}` + : `Cannot reach ${target}. Still no node set.` + ) + ); + return; + } + + setCurrentNodeUrl(target); + console.log( + chalk.green(`Using node: ${target} (version ${status.version})`) + ); + + // chainId comes from RPC, not from the node, so the two can disagree. The switch + // is already committed at this point, so this is a courtesy warning only — bound + // it so an unresponsive RPC cannot leave the command hanging. + const chainIds = nodeChainIds(status); + if (chainIds.length > 0) { + let timer: NodeJS.Timeout | undefined; + try { + const { chainId } = await Promise.race([ + initializeSigner(), + new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error("RPC timeout")), 5000); + }), + ]); + if (!chainIds.includes(String(chainId))) { + console.log( + chalk.yellow( + `Warning: this node serves chain(s) ${chainIds.join(", ")} but RPC is on chain ${chainId}. Commands may fail.` + ) + ); + } + } catch { + // A bad/slow RPC must not make a successful node switch look like a failure. + } finally { + // Leaving the loser pending would keep the event loop alive for 5s. + clearTimeout(timer); + } + } + }); + + // getNode command + program + .command("getNode") + .alias("currentNode") + .description("Shows the Ocean Node currently in use") + .action(async () => { + const current = getCurrentNodeUrl(); + if (!current) { + console.log( + chalk.yellow("No Ocean Node set. Run `setNode ` to pick one.") + ); + return; + } + console.log(`Current Ocean Node: ${current}`); + // Best effort: a node that is down must not fail the command. + const status = await validateNode(current); + if (status) { + console.log( + `Version: ${status.version}, chain(s): ${nodeChainIds(status).join(", ") || "none"}` + ); + } else { + console.log(chalk.yellow("Node is not reachable right now.")); + } + }); + // getDDO command program .command("getDDO") @@ -561,6 +702,315 @@ export async function createCLI() { ]); }); + // ========================================================================= + // Service-on-Demand commands + // ========================================================================= + + // getServiceTemplates command + program + .command("getServiceTemplates") + .alias("serviceTemplates") + .description( + "Lists the node's Service-on-Demand templates and compatible environments" + ) + .argument( + "[node]", + "Optional Ocean Node URL or peer id to query (defaults to NODE_URL)" + ) + .option("-n, --node ", "Ocean Node URL or peer id to query") + .action(async (node, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.getServiceTemplates(options.node || node); + }); + + // startService command + program + .command("startService") + .description( + "Starts an on-demand service (long-running container) on a compute environment, paid via escrow" + ) + .argument("", "Compute environment ID (must have services enabled)") + .argument("", "Requested duration in seconds", parseInt) + .argument("", "Payment token address") + .option("--template ", "Start from an operator-published template") + .option("-i, --image ", "Container image (alternative to --template)") + .option("--tag ", "Image tag (mutually exclusive with --checksum/--dockerfile)") + .option("--checksum ", "Image digest, e.g. sha256:<64 hex>") + .option("--dockerfile ", "Path to a local Dockerfile (node must allow image builds)") + .option( + "--additional-docker-files ", + "Path to JSON file of {filename: content} used with --dockerfile" + ) + .option("--cmd ", 'Docker CMD override as JSON array, e.g. \'["python","app.py"]\'') + .option("--entrypoint ", "Docker ENTRYPOINT override as JSON array") + .option("-p, --ports ", "Comma-separated container ports to expose, e.g. 8888,8080") + .option( + "-r, --resources ", + 'Stringified JSON [{"id":"cpu","amount":1},...]; defaults to template requirements' + ) + .option( + "-u, --user-data ", + "JSON object of container env vars (encrypted to the node; never logged)" + ) + .option("--user-data-file ", "Path to JSON file with container env vars") + .option("--accept [boolean]", "Auto-confirm payment (true/false)", toBoolean) + .option("--wait [boolean]", "Poll until Running or failure (default true)", toBoolean, true) + .option("--timeout ", "Max seconds to wait for Running (default 600)", parseInt) + .action(async (computeEnvId, duration, paymentToken, options) => { + const envId = options.env || computeEnvId; + const token = paymentToken; + if (!envId || !duration || !token) { + console.error(chalk.red("Missing required arguments: ")); + return; + } + if (!Number.isInteger(duration) || duration <= 0) { + console.error(chalk.red("Duration must be a positive integer number of seconds.")); + return; + } + if (options.template && options.image) { + console.error(chalk.red("Provide either --template or --image, not both.")); + return; + } + + let ports: number[] | undefined; + let cmd: string[] | undefined; + let entrypoint: string[] | undefined; + try { + if (options.ports) ports = parsePorts(options.ports); + if (options.cmd) cmd = parseJsonStringArray("--cmd", options.cmd); + if (options.entrypoint) + entrypoint = parseJsonStringArray("--entrypoint", options.entrypoint); + } catch (e) { + console.error(chalk.red((e as Error).message)); + return; + } + + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.startService({ + envId, + duration, + paymentToken: token, + templateId: options.template, + image: options.image, + tag: options.tag, + checksum: options.checksum, + dockerfilePath: options.dockerfile, + additionalDockerFilesPath: options.additionalDockerFiles, + cmd, + entrypoint, + ports, + resources: options.resources, + userDataInline: options.userData, + userDataFilePath: options.userDataFile, + accept: options.accept, + wait: options.wait, + timeout: options.timeout, + }); + }); + + // getServiceStatus command (caller-owned, full detail) + program + .command("getServiceStatus") + .alias("myServices") + .description("Shows status + endpoints of YOUR on-demand service(s)") + .argument("[serviceId]", "Service ID; omit to list all your services") + .option("-s, --service ", "Service ID") + .option("-v, --verbose [boolean]", "Dump full job objects", toBoolean) + .action(async (serviceId, options) => { + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.getServiceStatus(options.service || serviceId, options.verbose); + }); + + // getServices command (SERVICES_LIST — node-wide, all owners) + program + .command("getServices") + .alias("listServices") + .description( + "Lists on-demand services across ALL owners on the node (docker spec hidden)" + ) + .argument( + "[node]", + "Optional Ocean Node URL or peer id to query (defaults to NODE_URL)" + ) + .option("-n, --node ", "Ocean Node URL or peer id to query") + .option( + "--status ", + "Filter by a single service status number (e.g. 40 for Running)", + parseInt + ) + .option("--include-all [boolean]", "Include all statuses, not just active reservations", toBoolean) + .option("--from ", "Only services created at/after this time (ISO string or Unix timestamp)") + .option("-v, --verbose [boolean]", "Dump full job objects", toBoolean) + .action(async (node, options) => { + if ( + options.status !== undefined && + ServiceStatusNumber[options.status] === undefined + ) { + console.error( + chalk.red( + `Unknown --status ${options.status}. Valid values: 10,11,12,13,14,15,20,30,40,50,70,75,99` + ) + ); + return; + } + const filters: { + status?: number; + includeAllStatuses?: boolean; + fromTimestamp?: string; + } = {}; + if (options.status !== undefined) filters.status = options.status; + if (options.includeAll !== undefined) + filters.includeAllStatuses = options.includeAll; + if (options.from !== undefined) filters.fromTimestamp = options.from; + + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.getServices(options.node || node, filters, options.verbose); + }); + + // serviceLogs command (streamable logs) + program + .command("serviceLogs") + .alias("computeServiceLogs") + .description("Streams live logs from an on-demand service's container") + .argument("", "Service ID") + .option("-s, --service ", "Service ID") + .option( + "--since ", + "Only logs since this time — Unix seconds or a relative duration like 30s / 2h" + ) + .action(async (serviceId, options) => { + const id = options.service || serviceId; + if (!id) { + console.error(chalk.red("Missing required argument: ")); + return; + } + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.serviceLogs(id, options.since); + }); + + // extendService command + program + .command("extendService") + .description("Extends a running on-demand service's expiry (paid via escrow)") + .argument("", "Service ID") + .argument("", "Additional duration in seconds", parseInt) + .argument("[paymentToken]", "Payment token (defaults to the token used at start)") + .option("-s, --service ", "Service ID") + .option("--duration ", "Additional duration in seconds", parseInt) + .option("-t, --token [paymentToken]", "Payment token") + .option("--accept [boolean]", "Auto-confirm payment (true/false)", toBoolean) + .action(async (serviceId, additionalDuration, paymentToken, options) => { + const id = options.service || serviceId; + const addl = options.duration || additionalDuration; + const token = options.token || paymentToken; + if (!id || !addl) { + console.error(chalk.red("Missing required arguments: ")); + return; + } + if (!Number.isInteger(addl) || addl <= 0) { + console.error(chalk.red("additionalDuration must be a positive integer number of seconds.")); + return; + } + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.extendService(id, addl, token, options.accept); + }); + + // restartService command + program + .command("restartService") + .description( + "Restarts a running service (same ports & expiry; no extra charge). " + + "With no container-spec flags the container bounces unchanged (REUSE); " + + "supplying any image-spec flag (--image/--tag/--checksum/--dockerfile/" + + "--additional-docker-files) rebuilds it on the new spec (RESPEC, #2119)" + ) + .argument("", "Service ID") + .option("-u, --user-data ", "REPLACE stored container env vars (JSON object)") + .option("--user-data-file ", "Path to JSON file with replacement env vars") + .option("--cmd ", "REPLACE stored Docker CMD as JSON array (#2114); empty array clears it") + .option("--entrypoint ", "REPLACE stored Docker ENTRYPOINT as JSON array (#2114)") + .option("--image ", "RESPEC: rebuild on this container image (#2119)") + .option("--tag ", "RESPEC: rebuild on this image tag (#2119)") + .option("--checksum ", "RESPEC: image digest/checksum (#2119)") + .option("--dockerfile ", "RESPEC: dockerfile contents to build from (#2119)") + .option( + "--additional-docker-files ", + "RESPEC: extra build files as a JSON object { path: contents } (#2119)" + ) + .option("--wait [boolean]", "Poll until Running (default true)", toBoolean, true) + .option("--timeout ", "Max seconds to wait (default 600)", parseInt) + .action(async (serviceId, options) => { + if (!serviceId) { + console.error(chalk.red("Missing required argument: ")); + return; + } + const params: ServiceRestartParams = {}; + try { + if (options.userData) { + const parsed = JSON.parse(options.userData); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("--user-data must be a JSON object"); + } + params.userData = parsed; + } else if (options.userDataFile) { + const parsed = JSON.parse(fs.readFileSync(options.userDataFile, "utf8")); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("--user-data-file must contain a JSON object"); + } + params.userData = parsed; + } + if (options.cmd !== undefined) + params.dockerCmd = parseJsonStringArray("--cmd", options.cmd); + if (options.entrypoint !== undefined) + params.dockerEntrypoint = parseJsonStringArray("--entrypoint", options.entrypoint); + if (options.image !== undefined) params.image = options.image; + if (options.tag !== undefined) params.tag = options.tag; + if (options.checksum !== undefined) params.checksum = options.checksum; + if (options.dockerfile !== undefined) params.dockerfile = options.dockerfile; + if (options.additionalDockerFiles !== undefined) { + const parsed = JSON.parse(options.additionalDockerFiles); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("--additional-docker-files must be a JSON object"); + } + params.additionalDockerFiles = parsed; + } + } catch (e) { + console.error(chalk.red((e as Error).message)); + return; + } + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.restartService( + serviceId, + Object.keys(params).length > 0 ? params : undefined, + options.wait, + options.timeout + ); + }); + + // stopService command + program + .command("stopService") + .description("Stops an on-demand service and releases its resources") + .argument("", "Service ID") + .option("-s, --service ", "Service ID") + .action(async (serviceId, options) => { + const id = options.service || serviceId; + if (!id) { + console.error(chalk.red("Missing required argument: ")); + return; + } + const { signer, chainId } = await initializeSigner(); + const commands = new Commands(signer, chainId); + await commands.stopService(id); + }); + // mintOcean command program .command("mintOcean") diff --git a/src/commands.ts b/src/commands.ts index 06a513e..3aa74ee 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -30,16 +30,63 @@ import { getTokenDecimals, AccesslistFactory, AccessListContract, + ComputeResourceRequest, + ServiceJob, + ServiceJobListed, + ServiceRestartParams, + ServiceStartParams, + ServiceStatusNumber, + ServiceTemplatePublic, } from "@oceanprotocol/lib"; import { Asset, DDOManager } from '@oceanprotocol/ddo-js'; import { Signer, ethers, getAddress } from "ethers"; +import { stdin as input, stdout as output } from "node:process"; +import { createInterface } from "readline/promises"; import { interactiveFlow } from "./interactiveFlow.js"; import { publishAsset } from "./publishAsset.js"; import chalk from 'chalk'; import { getPolicyServerOBJ, getPolicyServerOBJs, isVersionGte } from "./policyServerHelper.js"; +import { + findServiceEnvironments, + templateMismatchReason, + resolveServiceResources, + estimateServiceCost, + parseUserData, + describeUserDataKeys, + verifyServiceEscrow, + pollServiceStatus, + printServiceJob, + formatExpiry, + statusLabel, + isTerminal, +} from "./serviceHelpers.js"; const UPLOAD_TIMEOUT_MS = 30 * 60_000; +// A node log endpoint streams in follow mode: it stays open for as long as the +// container lives, so reading it to the end never returns. Left unbounded, undici +// eventually kills the body with UND_ERR_BODY_TIMEOUT and everything buffered so +// far is lost with it. Read for this window instead, then abort and print what +// arrived (an idle container legitimately sends nothing at all). +const LOGS_STREAM_WINDOW_MS = 15_000; + +/** + * Drains a (possibly endless) log stream into a string. Chunks already received + * when the read is aborted are kept — a bounded read is the normal way this ends. + */ +async function drainLogStream(stream: any): Promise { + const chunks: Uint8Array[] = []; + if (!stream[Symbol.asyncIterator]) { + return await new Response(stream).text(); + } + try { + for await (const chunk of stream) chunks.push(chunk); + } catch (error) { + if (chunks.length === 0) throw error; + } + return Buffer.concat(chunks).toString("utf-8"); +} + export class Commands { public signer: Signer; public config: Config; @@ -880,6 +927,113 @@ export class Commands { return; } + // verifyFundsForEscrowPayment deposits funds and authorizes this node when + // either is missing, but it sends both transactions through + // sendPreparedTransaction, which logs and swallows a failure (a nonce collision + // between those back-to-back txs is the common one on a fast local chain) and + // still reports isValid. The node then rejects computeStart with "User ... does + // not have enough funds" or "Found 0 authorizations". Confirm both really + // landed, and retry once each before giving up. + const payerAddress = await this.signer.getAddress(); + const payeeAddress = getAddress(computeEnv.consumerAddress); + const tokenAddress = getAddress(paymentToken); + const minLockSeconds = + parsedProviderInitializeComputeJob.payment.minLockSeconds.toString(); + + const requiredUnits = BigInt( + parsedProviderInitializeComputeJob.payment.amount.toString() + ); + const availableUnits = async (): Promise => { + // ethers returns the userFunds struct; `available` excludes what is locked. + const funds = await escrow.getUserFunds(payerAddress, tokenAddress); + return BigInt((funds.available ?? funds[0]).toString()); + }; + let available = await availableUnits(); + if (available < requiredUnits) { + const shortfallUnits = requiredUnits - available; + const shortfall = await unitsToAmount( + this.signer, + paymentToken, + shortfallUnits.toString() + ); + console.log( + chalk.yellow( + `Escrow balance is short of this job's cost — depositing ${shortfall}...` + ) + ); + const tokenContract = new ethers.Contract( + paymentToken, + ["function approve(address spender, uint256 amount) returns (bool)"], + this.signer + ); + const approveTx = await tokenContract.approve( + getAddress(parsedProviderInitializeComputeJob.payment.escrowAddress), + shortfallUnits + ); + await approveTx.wait(); + const depositTx = await escrow.deposit(paymentToken, shortfall); + if (depositTx) await depositTx.wait(); + available = await availableUnits(); + } + if (available < requiredUnits) { + const needed = await unitsToAmount( + this.signer, + paymentToken, + requiredUnits.toString() + ); + console.error( + chalk.red( + `Escrow balance for token ${paymentToken} is below this job's cost (${needed}) and the deposit did not go through. ` + + `Deposit manually and retry:\n` + + ` npm run cli depositEscrow ${paymentToken} ${needed}` + ) + ); + return; + } + let authorizations = await escrow.getAuthorizations( + tokenAddress, + payerAddress, + payeeAddress + ); + if (!authorizations || authorizations.length === 0) { + console.log( + chalk.yellow( + "Escrow authorization for the compute node is missing after the funds check — authorizing explicitly..." + ) + ); + // Ten times the job cost as the ceiling: locks accumulate against + // maxLockedAmount until they are claimed, so a ceiling of exactly one job's + // cost would reject the next job started before this one settles. + const jobCost = await unitsToAmount( + this.signer, + paymentToken, + parsedProviderInitializeComputeJob.payment.amount + ); + const authorizeTx = await escrow.authorize( + tokenAddress, + payeeAddress, + (Number(jobCost) * 10).toString(), + minLockSeconds, + "10" + ); + if (authorizeTx) await authorizeTx.wait(); + authorizations = await escrow.getAuthorizations( + tokenAddress, + payerAddress, + payeeAddress + ); + } + if (!authorizations || authorizations.length === 0) { + console.error( + chalk.red( + `Could not authorize the compute node ${payeeAddress} to lock escrow funds for token ${paymentToken}. ` + + `Authorize it manually and retry:\n` + + ` npm run cli authorizeEscrow ${paymentToken} ${payeeAddress} ${minLockSeconds} 10` + ) + ); + return; + } + console.log("Starting compute job using provider: ", providerURI); const additionalDatasets = assets.length > 1 ? assets.slice(1) : null; @@ -1156,29 +1310,833 @@ export class Commands { public async computeStreamableLogs(args: string[]) { const jobId = args[0]; - const logsResponse = await ProviderInstance.computeStreamableLogs( - this.oceanNodeUrl, - this.signer, - jobId + const controller = new AbortController(); + let windowElapsed = false; + const timer = setTimeout(() => { + windowElapsed = true; + controller.abort(); + }, LOGS_STREAM_WINDOW_MS); + try { + const logsResponse = await ProviderInstance.computeStreamableLogs( + this.oceanNodeUrl, + this.signer, + jobId, + controller.signal + ); + + if (!logsResponse) { + console.error("Error fetching streamable logs. No logs available."); + return; + } + + const text = await drainLogStream(logsResponse); + console.log("Streamable Logs:"); + console.log(text); + } catch (error) { + if (windowElapsed) { + console.error( + `No logs streamed for job ${jobId} within ${ + LOGS_STREAM_WINDOW_MS / 1000 + }s.` + ); + return; + } + console.error("Error fetching streamable logs:", error); + } finally { + clearTimeout(timer); + } + } + + // ========================================================================= + // Service-on-Demand (long-running containers on a compute environment) + // ========================================================================= + + // Interactive payment confirmation, mirroring the startCompute prompt. + // Returns true to proceed, false to abort. REPL-safe (no process.exit). + private async confirmServicePayment( + costHuman: number, + token: string, + durationSeconds: number, + accept?: boolean + ): Promise { + console.log( + chalk.yellow( + `\n--- Payment Details ---\n` + + ` estimated cost: ${costHuman} (token ${token})\n` + + ` duration: ${durationSeconds}s\n` + + ` Note: the final cost is computed by the node and shown after start.` + ) ); + if (accept) { + console.log(chalk.cyan("Auto-confirm enabled with --accept.")); + return true; + } + if (!process.stdin.isTTY) { + console.error( + chalk.red( + 'Cannot prompt for confirmation (non-TTY). Use "--accept true" to skip.' + ) + ); + return false; + } + const rl = createInterface({ input, output }); + const confirmation = await rl.question( + `\nProceed with payment of estimated ${costHuman} ${token} for ${durationSeconds}s? (y/n): ` + ); + rl.close(); + const answer = confirmation.trim().toLowerCase(); + if (answer !== "y" && answer !== "yes") { + console.log(chalk.red("Service start canceled by user.")); + return false; + } + return true; + } - if (!logsResponse) { - console.error("Error fetching streamable logs. No logs available."); - return; + public async getServiceTemplates(nodeUrlOverride?: string): Promise { + const nodeUrl = nodeUrlOverride || this.oceanNodeUrl; + try { + const templates = await ProviderInstance.getServiceTemplates(nodeUrl); + if (!templates || templates.length < 1) { + console.log( + chalk.yellow("Node has no Service-on-Demand templates configured.") + ); + return; + } + + let envs = []; + try { + envs = await ProviderInstance.getComputeEnvironments(nodeUrl); + } catch { + envs = []; + } + + for (const t of templates) { + const imageSpec = t.tag + ? `${t.image}:${t.tag}` + : t.checksum + ? `${t.image}@${t.checksum}` + : t.dockerfile + ? `${t.image} (dockerfile)` + : t.image; + console.log(`\n${chalk.bold(t.id)}${t.name ? ` — ${t.name}` : ""}`); + if (t.description) console.log(` ${t.description}`); + console.log(` image: ${imageSpec}`); + console.log(` exposedPorts: ${JSON.stringify(t.exposedPorts ?? [])}`); + if (t.userConfigurableEnvVars?.length) { + console.log(" userConfigurableEnvVars:"); + for (const v of t.userConfigurableEnvVars) { + console.log( + ` - ${v.key}${v.sensitive ? " (sensitive)" : ""}${ + v.validation ? ` [validation: ${v.validation}]` : "" + }` + ); + } + } + if (t.requiredResources?.length) { + console.log( + ` requiredResources: ${JSON.stringify(t.requiredResources)}` + ); + } + if (t.recommendedResources?.length) { + console.log( + ` recommendedResources: ${JSON.stringify(t.recommendedResources)}` + ); + } + const compatible = findServiceEnvironments(envs, t).map((e) => e.id); + if (compatible.length) { + console.log(` compatible environments: ${compatible.join(", ")}`); + } else { + console.log( + chalk.red( + " compatible environments: none — insufficient free resources or services disabled" + ) + ); + } + } + + // Stable, machine-parseable line (mirrors getComputeEnvironments). + console.log("Service templates: " + JSON.stringify(templates)); + } catch (error) { + console.error(chalk.red("Error fetching service templates:"), error); } + } - let text: string; - if (logsResponse[Symbol.asyncIterator]) { - const chunks: Uint8Array[] = []; - for await (const chunk of logsResponse) { - chunks.push(chunk); + public async startService(opts: { + envId: string; + duration: number; + paymentToken: string; + templateId?: string; + image?: string; + tag?: string; + checksum?: string; + dockerfilePath?: string; + additionalDockerFilesPath?: string; + cmd?: string[]; + entrypoint?: string[]; + ports?: number[]; + resources?: string; + userDataInline?: string; + userDataFilePath?: string; + accept?: boolean; + wait?: boolean; + timeout?: number; + }): Promise { + try { + const { chainId } = await this.signer.provider.getNetwork(); + const chainIdNum = Number(chainId); + + // 1. Resolve env + const envs = await ProviderInstance.getComputeEnvironments( + this.oceanNodeUrl + ); + if (!envs || envs.length < 1) { + console.error(chalk.red("No compute environments available.")); + return; } - text = Buffer.concat(chunks).toString("utf-8"); - } else { - text = await new Response(logsResponse).text(); + const env = envs.find((e) => e.id === opts.envId); + if (!env) { + console.error( + chalk.red(`No compute environment matches id: ${opts.envId}`) + ); + return; + } + if (env.features?.services === false) { + console.error( + chalk.red(`Environment ${env.id} has services disabled.`) + ); + return; + } + + // 2. Resolve container spec (template and/or explicit flags) + let template: ServiceTemplatePublic | undefined; + let image: string | undefined; + let tag: string | undefined; + let checksum: string | undefined; + let dockerfile: string | undefined; + let additionalDockerFiles: Record | undefined; + let exposedPorts: number[] | undefined; + let dockerCmd: string[] | undefined; + let dockerEntrypoint: string[] | undefined; + + if (opts.templateId) { + const templates = await ProviderInstance.getServiceTemplates( + this.oceanNodeUrl + ); + template = (templates ?? []).find((t) => t.id === opts.templateId); + if (!template) { + console.error( + chalk.red(`Template "${opts.templateId}" not found on the node.`) + ); + return; + } + image = template.image; + tag = template.tag; + checksum = template.checksum; + dockerfile = template.dockerfile; + additionalDockerFiles = template.additionalDockerFiles; + exposedPorts = template.exposedPorts; + // NOTE the field rename: template.command -> dockerCmd, .entrypoint -> dockerEntrypoint + dockerCmd = template.command; + dockerEntrypoint = template.entrypoint; + + const reason = templateMismatchReason(env, template); + if (reason) { + console.error( + chalk.red( + `Environment ${env.id} does not satisfy template "${template.id}": ${reason}` + ) + ); + return; + } + } + + // Explicit flags override template values. + image = opts.image || image; + if (!image) { + console.error( + chalk.red("An image is required: pass --template or --image .") + ); + return; + } + if (opts.tag !== undefined) tag = opts.tag; + if (opts.checksum !== undefined) checksum = opts.checksum; + if (opts.dockerfilePath) { + try { + dockerfile = fs.readFileSync(opts.dockerfilePath, "utf8"); + } catch (e) { + console.error( + chalk.red(`Cannot read Dockerfile at ${opts.dockerfilePath}`), + e + ); + return; + } + } + if (opts.additionalDockerFilesPath) { + try { + additionalDockerFiles = JSON.parse( + fs.readFileSync(opts.additionalDockerFilesPath, "utf8") + ); + } catch (e) { + console.error( + chalk.red( + `Cannot read additional docker files JSON at ${opts.additionalDockerFilesPath}` + ), + e + ); + return; + } + } + + const specCount = [tag, checksum, dockerfile].filter(Boolean).length; + if (specCount > 1) { + console.error( + chalk.red( + "Provide at most one of --tag, --checksum or --dockerfile." + ) + ); + return; + } + + if (opts.ports) exposedPorts = opts.ports; + if (opts.cmd) dockerCmd = opts.cmd; + if (opts.entrypoint) dockerEntrypoint = opts.entrypoint; + + // 3. Resolve resources + let resources: ComputeResourceRequest[]; + if (opts.resources) { + try { + const parsed = JSON.parse(opts.resources); + if ( + !Array.isArray(parsed) || + !parsed.every( + (r) => + r && + typeof r.id === "string" && + typeof r.amount === "number" + ) + ) { + throw new Error("must be an array of {id, amount}"); + } + resources = parsed; + } catch (e) { + console.error( + chalk.red(`Invalid --resources JSON: ${(e as Error).message}`) + ); + return; + } + } else { + resources = resolveServiceResources(template, env); + } + console.log(`Requested resources: ${JSON.stringify(resources)}`); + + // 4. Duration + if (!Number.isInteger(opts.duration) || opts.duration <= 0) { + console.error( + chalk.red("Duration must be a positive integer number of seconds.") + ); + return; + } + if (opts.duration > 86400) { + console.log( + chalk.yellow( + `Warning: duration ${opts.duration}s exceeds the node's typical maxDurationSeconds (86400) — the node may clamp or reject it.` + ) + ); + } + + // 5. userData (never logged — keys only) + let userDataFromFile: Record | undefined; + if (opts.userDataFilePath) { + try { + userDataFromFile = JSON.parse( + fs.readFileSync(opts.userDataFilePath, "utf8") + ); + } catch (e) { + console.error( + chalk.red(`Cannot read user-data file at ${opts.userDataFilePath}`), + e + ); + return; + } + } + let userData: Record | undefined; + try { + userData = parseUserData( + opts.userDataInline, + userDataFromFile, + template + ); + } catch (e) { + console.error(chalk.red((e as Error).message)); + return; + } + const userDataKeys = describeUserDataKeys(userData); + if (userDataKeys) console.log(`userData keys: ${userDataKeys}`); + + // 6. Cost + escrow gate + const cost = estimateServiceCost( + env, + chainIdNum, + opts.paymentToken, + resources, + opts.duration + ); + if (cost === null) { + console.error( + chalk.red( + `Environment ${env.id} has no pricing for token ${opts.paymentToken} on chain ${chainIdNum}.` + ) + ); + return; + } + const escrowOk = await verifyServiceEscrow( + this.signer, + chainIdNum, + opts.paymentToken, + env.consumerAddress, + cost, + opts.duration + ); + if (!escrowOk) return; + + // 7. Confirmation prompt + const proceed = await this.confirmServicePayment( + cost, + opts.paymentToken, + opts.duration, + opts.accept + ); + if (!proceed) return; + + // 8. Start (async on the node — returns immediately in status Starting) + const params: ServiceStartParams = { + environment: env.id, + image, + tag, + checksum, + dockerfile, + additionalDockerFiles, + dockerCmd, + dockerEntrypoint, + exposedPorts, + resources, + duration: opts.duration, + userData, // plain object; ocean.js encrypts it to the node + payment: { chainId: chainIdNum, token: opts.paymentToken }, + }; + + const jobs = await ProviderInstance.serviceStart( + this.oceanNodeUrl, + this.signer, + params, + AbortSignal.timeout(120_000) + ); + const job = jobs?.[0]; + if (!job) { + console.error(chalk.red("Service start returned no job."), jobs); + return; + } + + // Always print the id first — polling may die but the user needs it. + console.log(chalk.green(`Service started. ServiceID: ${job.serviceId}`)); + if (job.payment?.cost !== undefined) { + console.log(`Node-computed cost: ${job.payment.cost}`); + } + + // 9. Wait for Running (unless --wait false) + if (opts.wait === false) { + console.log( + `Check later with: npm run cli getServiceStatus ${job.serviceId}` + ); + return job; + } + + try { + const running = await pollServiceStatus( + this.oceanNodeUrl, + this.signer, + job.serviceId, + ServiceStatusNumber.Running, + (opts.timeout ?? 600) * 1000 + ); + printServiceJob(running); + return running; + } catch (e) { + console.error(chalk.red((e as Error).message)); + console.log( + `Check later with: npm run cli getServiceStatus ${job.serviceId}` + ); + return job; + } + } catch (error) { + console.error(chalk.red("Error starting service:"), error); + } + } + + public async getServiceStatus( + serviceId?: string, + verbose?: boolean + ): Promise { + try { + const jobs = await ProviderInstance.getServiceStatus( + this.oceanNodeUrl, + this.signer, + serviceId + ); + if (!jobs || jobs.length < 1) { + const who = await this.signer.getAddress(); + console.log( + chalk.yellow( + `No services found for ${who}${ + serviceId ? ` with id ${serviceId}` : "" + }` + ) + ); + return []; + } + for (const job of jobs) printServiceJob(job, { verbose }); + return jobs; + } catch (error) { + console.error(chalk.red("Error fetching service status:"), error); + return []; + } + } + + public async getServices( + nodeUrlOverride?: string, + filters?: { + status?: number; + includeAllStatuses?: boolean; + fromTimestamp?: string; + }, + verbose?: boolean + ): Promise { + const nodeUrl = nodeUrlOverride || this.oceanNodeUrl; + try { + const jobs = await ProviderInstance.getServices( + nodeUrl, + this.signer, + filters as any + ); + if (!jobs || jobs.length < 1) { + const filterDesc = + filters && Object.keys(filters).length + ? ` (filters: ${JSON.stringify(filters)})` + : ""; + console.log(chalk.yellow(`No services found on ${nodeUrl}${filterDesc}`)); + console.log("Services list: " + JSON.stringify(jobs ?? [])); + return []; + } + for (const job of jobs) printServiceJob(job as ServiceJob, { verbose }); + // Stable, machine-parseable line for tests/scripts. + console.log("Services list: " + JSON.stringify(jobs)); + return jobs; + } catch (error) { + console.error(chalk.red("Error listing services:"), error); + return []; + } + } + + public async serviceLogs(serviceId: string, since?: string): Promise { + const controller = new AbortController(); + let windowElapsed = false; + const timer = setTimeout(() => { + windowElapsed = true; + controller.abort(); + }, LOGS_STREAM_WINDOW_MS); + try { + const stream = await ProviderInstance.serviceGetStreamableLogs( + this.oceanNodeUrl, + this.signer, + serviceId, + since, + controller.signal + ); + if (!stream) { + console.log( + chalk.yellow(`No logs available for service ${serviceId}`) + ); + return; + } + const text = await drainLogStream(stream); + if (text.trim().length === 0) { + console.log( + chalk.yellow(`No logs available for service ${serviceId}`) + ); + return; + } + console.log("Service Logs:"); + console.log(text); + } catch (error) { + // Our own deadline firing before anything arrived is not a failure: a + // container that has produced no output keeps the stream open and silent. + if (windowElapsed) { + console.log( + chalk.yellow( + `No logs available for service ${serviceId} (nothing streamed within ${ + LOGS_STREAM_WINDOW_MS / 1000 + }s)` + ) + ); + return; + } + console.error(chalk.red("Error fetching service logs:"), error); + } finally { + clearTimeout(timer); + } + } + + public async extendService( + serviceId: string, + additionalDuration: number, + paymentToken?: string, + accept?: boolean + ): Promise { + try { + if (!Number.isInteger(additionalDuration) || additionalDuration <= 0) { + console.error( + chalk.red("additionalDuration must be a positive integer (seconds).") + ); + return; + } + + // 1. Fetch the job + const jobs = await ProviderInstance.getServiceStatus( + this.oceanNodeUrl, + this.signer, + serviceId + ); + const job = (jobs ?? []).find((j) => j.serviceId === serviceId); + if (!job) { + console.error(chalk.red(`Service ${serviceId} not found.`)); + return; + } + if (isTerminal(job.status)) { + console.error( + chalk.red( + `Service ${serviceId} is ${statusLabel( + job.status, + job.statusText + )} (${job.status}) — nothing to extend.` + ) + ); + return; + } + + const { chainId } = await this.signer.provider.getNetwork(); + const chainIdNum = Number(chainId); + const token = paymentToken || job.payment?.token; + if (!token) { + console.error( + chalk.red( + "No payment token: pass one explicitly (the job has no stored token)." + ) + ); + return; + } + if ( + job.payment?.chainId !== undefined && + Number(job.payment.chainId) !== chainIdNum + ) { + console.log( + chalk.yellow( + `Warning: job was paid on chain ${job.payment.chainId} but the signer is on ${chainIdNum}; the environment must price on this chain.` + ) + ); + } + + // Resolve the env once — we need its consumerAddress (escrow payee) and, + // as a fallback, its fee schedule for the cost estimate. + const envs = await ProviderInstance.getComputeEnvironments( + this.oceanNodeUrl + ); + const env = (envs ?? []).find((e) => e.id === job.environment); + if (!env) { + console.error( + chalk.red( + `Environment ${job.environment} for this service was not found on the node.` + ) + ); + return; + } + + // 2/3. Estimate cost: prefer the running job's priced resources when the + // token matches; otherwise fall back to the env fee schedule. + let cost: number | null = null; + const sameToken = + job.payment?.token && + job.payment.token.toLowerCase() === token.toLowerCase(); + const pricedResources = + Array.isArray(job.resources) && + job.resources.length > 0 && + job.resources.every((r) => typeof r?.price === "number"); + if (sameToken && pricedResources) { + const minutes = Math.ceil(additionalDuration / 60); + cost = job.resources.reduce( + (sum: number, r: any) => + sum + Number(r.price ?? 0) * Number(r.amount ?? 0) * minutes, + 0 + ); + } else { + const resources = (job.resources ?? []).map((r: any) => ({ + id: r.id, + amount: r.amount, + })); + cost = estimateServiceCost( + env, + chainIdNum, + token, + resources, + additionalDuration + ); + } + if (cost === null) { + console.error( + chalk.red( + `Could not estimate extend cost for token ${token} on chain ${chainIdNum}.` + ) + ); + return; + } + + const escrowOk = await verifyServiceEscrow( + this.signer, + chainIdNum, + token, + env.consumerAddress, + cost, + additionalDuration + ); + if (!escrowOk) return; + + // 4. Confirmation prompt + const proceed = await this.confirmServicePayment( + cost, + token, + additionalDuration, + accept + ); + if (!proceed) return; + + // 5. Extend + const oldExpiry = job.expiresAt; + const extended = await ProviderInstance.serviceExtend( + this.oceanNodeUrl, + this.signer, + serviceId, + additionalDuration, + { chainId: chainIdNum, token }, + AbortSignal.timeout(120_000) + ); + const newJob = extended?.[0]; + if (!newJob) { + console.error(chalk.red("Extend returned no job."), extended); + return; + } + + // 6. Report + console.log(chalk.green(`Service ${serviceId} extended.`)); + console.log( + ` expiry: ${formatExpiry(oldExpiry)} → ${formatExpiry(newJob.expiresAt)}` + ); + console.log(` extendPayments: ${newJob.extendPayments?.length ?? 0}`); + return newJob; + } catch (error) { + console.error(chalk.red("Error extending service:"), error); + } + } + + public async restartService( + serviceId: string, + params?: ServiceRestartParams, + wait?: boolean, + timeout?: number + ): Promise { + try { + // 1. Fetch current job to learn the old containerId (poll for the new one) + const jobs = await ProviderInstance.getServiceStatus( + this.oceanNodeUrl, + this.signer, + serviceId + ); + const job = (jobs ?? []).find((j) => j.serviceId === serviceId); + if (!job) { + console.error(chalk.red(`Service ${serviceId} not found.`)); + return; + } + const oldContainerId = job.containerId; + + // 2. Restart. Omitting all container-spec fields bounces the container + // unchanged (REUSE); supplying any image-spec field rebuilds it (RESPEC, #2119). + const restarted = await ProviderInstance.serviceRestart( + this.oceanNodeUrl, + this.signer, + serviceId, + params, + AbortSignal.timeout(120_000) + ); + const newJob = restarted?.[0]; + if (!newJob) { + console.error(chalk.red("Restart returned no job."), restarted); + return; + } + console.log(chalk.green(`Service ${serviceId} restarting...`)); + + // 3. Wait for the NEW container to reach Running + if (wait === false) { + console.log( + `Check later with: npm run cli getServiceStatus ${serviceId}` + ); + return newJob; + } + try { + const running = await pollServiceStatus( + this.oceanNodeUrl, + this.signer, + serviceId, + ServiceStatusNumber.Running, + (timeout ?? 600) * 1000, + oldContainerId + ); + printServiceJob(running); + return running; + } catch (e) { + console.error(chalk.red((e as Error).message)); + console.log( + `Check later with: npm run cli getServiceStatus ${serviceId}` + ); + return newJob; + } + } catch (error) { + console.error(chalk.red("Error restarting service:"), error); + } + } + + public async stopService(serviceId: string): Promise { + try { + const jobs = await ProviderInstance.serviceStop( + this.oceanNodeUrl, + this.signer, + serviceId, + AbortSignal.timeout(120_000) + ); + const job = jobs?.[0]; + if (!job) { + console.error(chalk.red("Stop returned no job."), jobs); + return; + } + console.log( + chalk.green( + `Service ${serviceId} stopped — status ${statusLabel( + job.status, + job.statusText + )} (${job.status})` + ) + ); + return job; + } catch (error) { + console.error(chalk.red("Error stopping service:"), error); } - console.log("Streamable Logs:"); - console.log(text); } public async allowAlgo(args: string[]) { @@ -1563,6 +2521,23 @@ export class Commands { maxLockCounts: string ) { try { + // Neither the Escrow contract nor ocean.js rejects a zero/negative limit — + // authorize(…, 0, 0) would be mined and leave a payee that can never lock. + // Validate here so the CLI is the gate. + const limits: [string, string][] = [ + ["maxLockedAmount", maxLockedAmount], + ["maxLockSeconds", maxLockSeconds], + ["maxLockCounts", maxLockCounts], + ]; + for (const [name, value] of limits) { + if (!(Number(value) > 0)) { + console.error( + chalk.red(`${name} must be a positive number (got "${value}").`) + ); + return false; + } + } + const config = await getConfigByChainId(Number(this.config.chainId)); const escrowAddress = config.Escrow; @@ -1576,6 +2551,32 @@ export class Commands { maxLockSeconds, maxLockCounts ); + // ocean.js sends NO transaction when an authorization already exists for + // (payer, token, payee) — authorizeTx is null and the existing limits stay + // as they are. Say so explicitly: dereferencing null here used to surface + // as a TypeError under "Authorization failed", which reads like a chain + // error and hides the fact that the old, possibly lower, maxLockSeconds / + // maxLockedAmount / maxLockCounts are still in force. + if (!authorizeTx) { + const existing = await escrow.getAuthorizations( + getAddress(token), + await this.signer.getAddress(), + getAddress(payee) + ); + console.log( + chalk.yellow( + `Payee ${payee} is already authorized for token ${token} — the existing ` + + `authorization was left untouched (it cannot be raised or lowered here).` + ) + ); + if (existing?.length) { + const a = existing[0]; + console.log( + ` maxLockedAmount (wei): ${a.maxLockedAmount} maxLockSeconds: ${a.maxLockSeconds} maxLockCounts: ${a.maxLockCounts}` + ); + } + return true; + } await authorizeTx.wait(); console.log(`Successfully authorized payee ${payee} for token ${token}`); diff --git a/src/helpers.ts b/src/helpers.ts index 16497a5..e47baed 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -25,8 +25,21 @@ import { LoggerInstance } from "@oceanprotocol/lib"; import { homedir } from "os"; - -const ERC20Template = readFileSync('./node_modules/@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json', 'utf8') as any; +import { createRequire } from "module"; + +// Resolve the ERC20 template ABI through the module system rather than a +// cwd-relative path, so the compiled CLI works from any working directory +// (e.g. when installed globally). require.resolve finds the JSON inside the +// installed @oceanprotocol/contracts package regardless of cwd. +const require = createRequire(import.meta.url); +const ERC20Template = JSON.parse( + readFileSync( + require.resolve( + "@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json" + ), + "utf8" + ) +); export async function downloadFile( url: string, diff --git a/src/index.ts b/src/index.ts index d9c8f0f..aca3ece 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ +#!/usr/bin/env node +import "./warnings.js"; import { Command, CommanderError } from "commander"; import chalk from "chalk"; import { stdin as input, stdout as output } from "node:process"; import { createInterface } from "readline/promises"; import { createCLI } from './cli.js'; +import { stopP2P } from './nodeConnection.js'; let program: Command const supportedCommands: string[] = [] @@ -59,13 +62,17 @@ function tokenize(line: string): string[] { } /** - * Strip an optional leading `npm run cli` prefix so that a pasted example - * command behaves identically to the bare command form. + * Strip an optional leading `npm run cli` or `ocean-cli` prefix so that a pasted + * example command (from either the contributor docs or the global-install docs) + * behaves identically to the bare command form. */ function stripNpmPrefix(tokens: string[]): string[] { if (tokens[0] === "npm" && tokens[1] === "run" && tokens[2] === "cli") { return tokens.slice(3) } + if (tokens[0] === "ocean-cli") { + return tokens.slice(1) + } return tokens } @@ -98,7 +105,7 @@ async function runTokens(tokens: string[]): Promise { } } -const PROMPT = "Enter command ('exit' | 'quit' or CTRL-C to terminate):\n" +const PROMPT = "Enter command ('exit' | 'quit' | ESC or CTRL-C to terminate):\n" /** * Tab-completion for the command name (the first token only). readline completes @@ -124,30 +131,64 @@ function completer(line: string): [string[], string] { */ async function runLoop(): Promise { const rl = createInterface({ input, output, completer }) + + // On a TTY, let the Escape key exit the REPL (Ctrl-C already terminates via + // SIGINT; `exit`/`quit`/`\q`/EOF still work). readline already emits keypress + // events on the input stream in terminal mode, so a listener is enough — no + // raw-mode juggling. Guarded by isTTY so piped stdin (tests, scripts) is + // unaffected. + const onKeypress = (_str: string, key?: { name?: string }): void => { + if (key?.name === "escape") { + output.write("\n") + rl.close() + } + } + if (input.isTTY) input.on("keypress", onKeypress) + rl.setPrompt(PROMPT) rl.prompt() - for await (const rawLine of rl) { - const line = rawLine.trim() + try { + for await (const rawLine of rl) { + const line = rawLine.trim() - if (line === "quit" || line === "exit" || line === "\\q") { - break - } + if (line === "quit" || line === "exit" || line === "\\q") { + break + } + + // Empty input: re-prompt instead of busy-waiting or dropping the session. + if (line === "") { + rl.prompt() + continue + } - // Empty input: re-prompt instead of busy-waiting or dropping the session. - if (line === "") { + const tokens = stripNpmPrefix(tokenize(line)) + rl.pause() + await runTokens(tokens) + rl.resume() rl.prompt() - continue } - - const tokens = stripNpmPrefix(tokenize(line)) - rl.pause() - await runTokens(tokens) - rl.resume() - rl.prompt() + } finally { + if (input.isTTY) input.off("keypress", onKeypress) + rl.close() } +} - rl.close() +/** + * Wait until everything written to stdout/stderr has actually been handed over, so a + * forced process.exit() cannot truncate it. Writing an empty chunk queues the callback + * behind any pending writes on the stream. + */ +async function flushOutput(): Promise { + await Promise.all( + [process.stdout, process.stderr].map( + (stream) => + new Promise((resolve) => { + if (stream.writableLength === 0) return resolve() + stream.write("", () => resolve()) + }) + ) + ) } async function main(): Promise { @@ -155,16 +196,28 @@ async function main(): Promise { program = await createCLI(); for (const command of program.commands) { supportedCommands.push(command.name()) - const alias = command.alias() - if (alias) supportedCommands.push(alias) + // aliases() (plural): alias() would only ever return the first one. + const aliases = command.aliases() + supportedCommands.push(...aliases) } - // Handle help flag without initializing signer, and exit so it prints - // once (not again via parseAsync/runLoop below). - if (process.argv.includes('--help') || process.argv.includes('-h')) { + // Handle help/version flags without initializing a signer, and exit so + // they print once and never drop into the REPL below. createCLI() already + // skips env validation for these invocations. The bare positional forms + // `help`/`h` are treated the same as `--help` (print and exit) to match + // createCLI()'s configuration-free behavior; `help ` still routes + // to the registered help command below. + const cmdTokens = process.argv.slice(2); + const isBareHelp = + cmdTokens.length === 1 && (cmdTokens[0] === 'help' || cmdTokens[0] === 'h'); + if (process.argv.includes('--help') || process.argv.includes('-h') || isBareHelp) { program.outputHelp(); return; } + if (process.argv.includes('--version') || process.argv.includes('-V')) { + console.log(program.version()); + return; + } if (process.env.AVOID_LOOP_RUN === 'true') { // one shot @@ -177,9 +230,13 @@ async function main(): Promise { configureForLoop(program) // Run the initial command passed on argv once (if any), surfacing errors. + // When started with no command at all, show the help menu up front so the + // user sees what's available instead of facing a bare prompt. const initialTokens = process.argv.slice(2) if (initialTokens.length > 0) { await runTokens(initialTokens) + } else { + console.log(program.helpInformation()) } // Then loop on stdin until the user exits or input is exhausted. @@ -187,7 +244,24 @@ async function main(): Promise { } catch (error) { console.error(chalk.red(`Program Error: ${error.message}`)); + // Flush before exiting: process.exit() discards whatever a piped stdout/stderr + // still has buffered, which could swallow the message just written. Exiting + // here (rather than falling through to the finally) keeps failures immediate — + // the process is going away, so libp2p needs no orderly shutdown. + await flushOutput() process.exit(1); + } finally { + // Once libp2p has started the process can no longer end on its own: stopping + // it cleanly still leaves a MessagePort holding the event loop open. So stop + // it and, if it had been running, exit explicitly — after draining stdout, + // since a piped stdout (tests, scripts) can still hold buffered output that + // process.exit() would discard. Reached on every non-throwing path out of the + // try above; when nothing was started, Node exits on its own and drains the + // streams as part of that. + if (await stopP2P()) { + await flushOutput() + process.exit(process.exitCode ?? 0) + } } } diff --git a/src/interactiveFlow.ts b/src/interactiveFlow.ts index cbe91a4..9f5c6f4 100644 --- a/src/interactiveFlow.ts +++ b/src/interactiveFlow.ts @@ -2,7 +2,7 @@ import enquirer from 'enquirer'; const { prompt } = enquirer; -import { PublishAssetParams } from './publishAsset'; +import { PublishAssetParams } from './publishAsset.js'; import chalk from 'chalk'; import figlet from 'figlet'; diff --git a/src/nodeConnection.ts b/src/nodeConnection.ts new file mode 100644 index 0000000..0321798 --- /dev/null +++ b/src/nodeConnection.ts @@ -0,0 +1,233 @@ +import chalk from "chalk"; +import { ProviderInstance, isP2pUri, NodeStatus } from "@oceanprotocol/lib"; + +/** + * Ocean Node selection and libp2p transport lifecycle. + * + * The active node lives in `process.env.NODE_URL`, which stays the single source of + * truth: `Commands` (its constructor) and `getMetadataURI()` re-read that variable on + * every use, so mutating it switches every subsequent command with no other wiring. + * + * libp2p is *transport*, not a connection to one node: every P2P call in ocean.js takes + * a `nodeUri` and dials that peer on demand (direct dial for a full multiaddr, DHT + * lookup for a bare peer id). So one libp2p node serves any number of Ocean nodes and + * switching between them never restarts or stops it. + */ + +// Default Ocean bootstrap nodes (must be included explicitly since passing +// bootstrapPeers to setupP2P replaces the built-in defaults) +const OCEAN_BOOTSTRAP_PEERS = [ + "/dns4/bootstrap1.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmLhRDqfufZiQnxvQs2XHhd6hwkLSPfjAQg1gH8wgRixiP", + "/dns4/bootstrap2.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmHwzeVw7RpGopjZe6qNBJbzDDBdqtrSk7Gcx1emYsfgL4", + "/dns4/bootstrap3.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmBKSeEP3v4tYEPsZsZv9VELinyMCsrVTJW9BvQeFXx28U", + "/dns4/bootstrap4.oncompute.ai/tcp/9001/ws/p2p/16Uiu2HAmSTVTArioKm2wVcyeASHYEsnx2ZNq467Z4GMDU4ErEPom", +]; + +// A plain HTTP request to a node is quick; a P2P dial may need a DHT lookup first +// (ocean.js defaults dhtLookupTimeout to 60s), so it gets a longer leash. +const HTTP_STATUS_TIMEOUT_MS = 10_000; +const P2P_STATUS_TIMEOUT_MS = 30_000; + +let p2pReady: Promise | null = null; +let p2pFailure: Error | null = null; + +function p2pDisabled(): boolean { + return process.env.DISABLE_P2P === "true"; +} + +/** True when the given URI is a full multiaddr (as opposed to a bare peer id). */ +function isFullMultiaddr(nodeUrl: string): boolean { + return nodeUrl.startsWith("/") && nodeUrl.includes("/p2p/"); +} + +/** + * Bootstrap peers for the libp2p node: the initial node (so a local node given as a + * bare peer id is dialable via the localhost convention), any BOOTSTRAP_PEERS, and the + * Ocean defaults. + */ +function buildBootstrapPeers(initialNodeUrl?: string): string[] { + const extra = process.env.BOOTSTRAP_PEERS?.split(",").filter(Boolean) || []; + const localPeer = + initialNodeUrl && isP2pUri(initialNodeUrl) + ? isFullMultiaddr(initialNodeUrl) + ? [initialNodeUrl] + : [`/ip4/127.0.0.1/tcp/9001/ws/p2p/${initialNodeUrl}`] + : []; + return [...localPeer, ...extra, ...OCEAN_BOOTSTRAP_PEERS]; +} + +/** + * Start the shared libp2p node. Called once at startup and deliberately *not* awaited: + * connecting to bootstrap peers and warming the DHT takes seconds, and that should + * happen while the user reads the prompt rather than on their first P2P command. Any + * P2P-bound path awaits `ensureP2PReady()` before dialing. + * + * No-op when DISABLE_P2P=true or when libp2p is already up. + */ +export function startP2P(initialNodeUrl?: string): void { + if (p2pDisabled() || p2pReady) return; + if (ProviderInstance.getLibp2pNode()) { + p2pReady = Promise.resolve(); + return; + } + + const bootstrapPeers = buildBootstrapPeers(initialNodeUrl); + console.log( + chalk.cyan(`Starting libp2p (${bootstrapPeers.length} bootstrap peers)...`) + ); + + // The promise is stored, not awaited, so it MUST swallow its own rejection here: + // an unhandled rejection on a fire-and-forget promise would take the process down. + // The failure is remembered and re-surfaced to whoever awaits ensureP2PReady(). + p2pReady = ProviderInstance.setupP2P({ + bootstrapPeers, + libp2p: { + connectionGater: { + // Allow localhost connections / local nodes + denyDialMultiaddr: () => false, + }, + }, + } as any).then( + () => { + console.log(chalk.cyan("libp2p node started.")); + }, + (error) => { + p2pFailure = error instanceof Error ? error : new Error(String(error)); + console.error( + chalk.yellow(`libp2p failed to start: ${p2pFailure.message}`) + ); + } + ); +} + +/** + * Await the shared libp2p node before making a P2P call. Throws with a clear reason + * when P2P is unavailable, so callers can report it instead of timing out. + */ +export async function ensureP2PReady(): Promise { + if (p2pDisabled()) { + throw new Error("P2P transport is disabled (DISABLE_P2P=true)"); + } + // Pass the active node so a lazy start (one-shot run against a P2P node) still gets + // the localhost multiaddr for a bare peer id in its bootstrap list — without it a + // node on this machine could only be found through the DHT. + if (!p2pReady) startP2P(getCurrentNodeUrl()); + await p2pReady; + if (p2pFailure) { + throw new Error(`libp2p is not running: ${p2pFailure.message}`); + } +} + +/** + * Stop the shared libp2p node when the CLI is finished (see index.ts). + * + * Returns true when libp2p had been started, because stopping it is *not* enough to + * let the process end: it leaves a `MessagePort` behind that keeps the event loop + * alive even after a clean `stop()` (verified with `process.getActiveResourcesInfo()` + * — `stop()` itself completes in ~2ms and reports status "stopped"). The caller must + * therefore exit explicitly when this returns true. + */ +export async function stopP2P(waitForStartMs = 15_000): Promise { + const pending = p2pReady; + p2pReady = null; + if (!pending && !ProviderInstance.getLibp2pNode()) return false; + + if (pending) { + // A start still in flight has no node to stop yet, and stopping "nothing" would + // leave it to come up *after* cleanup and hold the process open forever. So wait + // for it — but boundedly, since a start dialing unreachable bootstrap peers must + // not stall exit. (startP2P's promise handles its own rejection, so this is safe + // to await.) + let timer: NodeJS.Timeout | undefined; + await Promise.race([ + pending, + new Promise((resolve) => { + timer = setTimeout(resolve, waitForStartMs); + }), + ]); + clearTimeout(timer); + // Belt and braces for the timed-out case: stop whatever the start eventually + // produces, so a slow start delays exit instead of preventing it. + pending + .then(() => ProviderInstance.getLibp2pNode()?.stop()) + .catch(() => undefined); + } + + const node = ProviderInstance.getLibp2pNode(); + if (node) { + try { + await node.stop(); + } catch (error) { + // Shutting down is best effort — never turn it into a command failure. + console.error( + chalk.yellow(`libp2p did not stop cleanly: ${error?.message ?? error}`) + ); + } + } + return true; +} + +/** The active Ocean Node, or "" when none has been set yet. */ +export function getCurrentNodeUrl(): string { + return process.env.NODE_URL || ""; +} + +/** Make `nodeUrl` the active Ocean Node for every subsequent command. */ +export function setCurrentNodeUrl(nodeUrl: string): void { + process.env.NODE_URL = nodeUrl; +} + +/** Whether a node is currently selected (gates most commands, see cli.ts). */ +export function hasNode(): boolean { + return getCurrentNodeUrl().length > 0; +} + +/** + * Health-check a candidate node without touching any existing state. Over HTTP this is + * a plain status request; over P2P the on-demand dial *is* the reachability check. + * Returns the node status (for display), or null when the node cannot be reached. + */ +export async function validateNode( + nodeUrl: string +): Promise { + try { + let timeout = HTTP_STATUS_TIMEOUT_MS; + if (isP2pUri(nodeUrl)) { + await ensureP2PReady(); + timeout = P2P_STATUS_TIMEOUT_MS; + if (!isFullMultiaddr(nodeUrl)) { + console.log(chalk.cyan(`Looking up peer ${nodeUrl.slice(0, 12)}...`)); + } + } + const status = await ProviderInstance.getNodeStatus( + nodeUrl, + AbortSignal.timeout(timeout) + ); + return status || null; + } catch (error) { + console.error( + chalk.yellow( + `Could not get status of ${nodeUrl}: ${error?.message ?? error}` + ) + ); + return null; + } +} + +/** Chain ids the node serves, as reported by its status (provider + indexer). */ +export function nodeChainIds(status: NodeStatus): string[] { + // Drop entries with no chainId before stringifying: String(undefined) would put the + // literal "undefined" in the list, which then shows up in getNode output and in the + // mismatch warning — and would make the list look non-empty when it holds no real ids. + const ids = [ + ...(status.provider || []) + .map((p) => p.chainId) + .filter(Boolean) + .map(String), + ...(status.indexer || []) + .map((i) => i.chainId) + .filter(Boolean) + .map(String), + ]; + return [...new Set(ids)]; +} diff --git a/src/policyServerHelper.ts b/src/policyServerHelper.ts index 8b38692..d1e3124 100644 --- a/src/policyServerHelper.ts +++ b/src/policyServerHelper.ts @@ -1,5 +1,5 @@ import { Asset } from "@oceanprotocol/ddo-js" -import { PolicyServerActions, PolicyServerGetPdAction, PolicyServerInitiateAction, PolicyServerInitiateActionData, PolicyServerInitiateComputeActionData, PolicyServerPresentationDefinition, SsiVerifiableCredential, SsiWalletDid, SsiWalletSession } from "./policyServerInterfaces" +import { PolicyServerActions, PolicyServerGetPdAction, PolicyServerInitiateAction, PolicyServerInitiateActionData, PolicyServerInitiateComputeActionData, PolicyServerPresentationDefinition, SsiVerifiableCredential, SsiWalletDid, SsiWalletSession } from "./policyServerInterfaces.js" import axios from "axios" import { Signer } from "ethers" diff --git a/src/serviceHelpers.ts b/src/serviceHelpers.ts new file mode 100644 index 0000000..377f821 --- /dev/null +++ b/src/serviceHelpers.ts @@ -0,0 +1,464 @@ +import util from "util"; +import chalk from "chalk"; +import { Signer, getAddress } from "ethers"; +import { + ProviderInstance, + EscrowContract, + amountToUnits, + unitsToAmount, + getTokenDecimals, + ComputeEnvironment, + ComputeResource, + ComputeResourceRequest, + ServiceJob, + ServiceStatusNumber, + ServiceTemplatePublic, + TemplateResourceRequirement, +} from "@oceanprotocol/lib"; +import { getConfigByChainId } from "./helpers.js"; + +// --------------------------------------------------------------------------- +// 4.1 Status labels +// --------------------------------------------------------------------------- + +export const SERVICE_STATUS_LABELS: Record = { + 10: "Starting", + 11: "Pulling image", + 12: "Image pull FAILED", + 13: "Building image", + 14: "Image build FAILED", + 15: "Image VULNERABLE", + 20: "Locking escrow", + 30: "Claiming payment", + 40: "Running", + 50: "Stopping", + 70: "Stopped", + 75: "Expired", + 99: "Error", +}; + +// Statuses that mean the service failed and will never reach Running. +export const TERMINAL_FAILURE_STATUSES = [12, 14, 15, 99]; + +// Any status the poller should stop on (failure or benign end state). +export function isTerminal(status: number): boolean { + return TERMINAL_FAILURE_STATUSES.includes(status) || [70, 75].includes(status); +} + +export function statusLabel(status: number, statusText?: string): string { + // Prefer the node-provided statusText; fall back to the local map. + return statusText || SERVICE_STATUS_LABELS[status] || `status ${status}`; +} + +// Colorize a status string: green for Running, red for failures, yellow otherwise. +function colorForStatus(status: number, text: string): string { + if (status === ServiceStatusNumber.Running) return chalk.green(text); + if (TERMINAL_FAILURE_STATUSES.includes(status)) return chalk.red(text); + return chalk.yellow(text); +} + +// --------------------------------------------------------------------------- +// 4.2 Environment <-> template resource matching +// (ported from ocean.js test/integration/Services.test.ts) +// --------------------------------------------------------------------------- + +export function availableFor( + env: ComputeEnvironment, + req: TemplateResourceRequirement +): number { + const resources: ComputeResource[] = env.resources ?? []; + if (req.id) { + const r = resources.find((x) => x.id === req.id); + return r ? (r.total ?? 0) - (r.inUse ?? 0) : 0; + } + return resources + .filter((x) => x.kind === req.kind && (!req.type || x.type === req.type)) + .reduce((sum, x) => sum + ((x.total ?? 0) - (x.inUse ?? 0)), 0); +} + +export function envSatisfiesTemplate( + env: ComputeEnvironment, + reqs?: TemplateResourceRequirement[] +): boolean { + return (reqs ?? []).every((req) => availableFor(env, req) >= req.min); +} + +// Human-readable reason a template does not fit an env (or null when it fits). +export function templateMismatchReason( + env: ComputeEnvironment, + template?: ServiceTemplatePublic +): string | null { + if (!template) return null; + for (const req of template.requiredResources ?? []) { + const have = availableFor(env, req); + if (have < req.min) { + const what = req.id ?? `${req.kind ?? "resource"}${req.type ? `/${req.type}` : ""}`; + return `${what}: need ${req.min}, have ${have}`; + } + } + return null; +} + +export function findServiceEnvironments( + envs: ComputeEnvironment[], + template?: ServiceTemplatePublic +): ComputeEnvironment[] { + return (envs ?? []).filter( + (e) => + e.features?.services !== false && + (!template || envSatisfiesTemplate(e, template.requiredResources)) + ); +} + +// --------------------------------------------------------------------------- +// 4.3 Default resources from a template +// --------------------------------------------------------------------------- + +export function resolveServiceResources( + template: ServiceTemplatePublic | undefined, + env: ComputeEnvironment +): ComputeResourceRequest[] { + const requiredById = (template?.requiredResources ?? []).filter( + (r) => typeof r.id === "string" + ); + if (requiredById.length) { + return requiredById.map((r) => ({ id: r.id as string, amount: r.min })); + } + return (env.resources ?? []) + .filter((r) => r.id === "cpu" || r.id === "ram") + .map((r) => ({ id: r.id, amount: 1 })); +} + +// --------------------------------------------------------------------------- +// 4.4 Cost estimation (same formula the node uses) +// --------------------------------------------------------------------------- + +// Returns the estimated cost in HUMAN token amount, or null when the env has no +// fee schedule for (chainId, token) — the caller must abort in that case. +export function estimateServiceCost( + env: ComputeEnvironment, + chainId: number, + token: string, + resources: { id: string; amount: number }[], + durationSeconds: number +): number | null { + const schedules = env.fees?.[String(chainId)]; + const schedule = schedules?.find( + (f) => f.feeToken.toLowerCase() === token.toLowerCase() + ); + if (!schedule) return null; + const priceFor = (id: string) => + Number(schedule.prices?.find((p) => p.id === id)?.price ?? 0); + const minutes = Math.ceil(durationSeconds / 60); + return resources.reduce( + (sum, r) => sum + priceFor(r.id) * r.amount * minutes, + 0 + ); +} + +// --------------------------------------------------------------------------- +// 4.5 userData parsing + validation +// --------------------------------------------------------------------------- + +// inlineJson wins over filePath. Returns undefined when neither is given. +// `template` (optional) is used only to validate/warn about keys. +export function parseUserData( + inlineJson?: string, + parsedFromFile?: Record, + template?: ServiceTemplatePublic +): Record | undefined { + let data: Record | undefined; + if (typeof inlineJson === "string" && inlineJson.trim().length > 0) { + let parsed: unknown; + try { + parsed = JSON.parse(inlineJson); + } catch { + throw new Error("--user-data must be a valid JSON object"); + } + if ( + typeof parsed !== "object" || + parsed === null || + Array.isArray(parsed) + ) { + throw new Error("--user-data must be a JSON object (not an array or primitive)"); + } + data = parsed as Record; + } else if (parsedFromFile) { + if ( + typeof parsedFromFile !== "object" || + parsedFromFile === null || + Array.isArray(parsedFromFile) + ) { + throw new Error("--user-data-file must contain a JSON object"); + } + data = parsedFromFile; + } + + if (!data) return undefined; + + if (template) { + const configurable = template.userConfigurableEnvVars ?? []; + const byKey = new Map(configurable.map((v) => [v.key, v])); + for (const key of Object.keys(data)) { + const spec = byKey.get(key); + if (!spec) { + // Warn (don't fail) about keys the template does not advertise. + console.log( + chalk.yellow( + `Warning: userData key "${key}" is not listed in the template's userConfigurableEnvVars.` + ) + ); + continue; + } + if (spec.validation) { + let re: RegExp | undefined; + try { + re = new RegExp(spec.validation); + } catch { + re = undefined; + } + // Only validate string values; never print the value itself. + const val = data[key]; + if (re && typeof val === "string" && !re.test(val)) { + throw new Error( + `userData value for "${key}" does not match the template's validation pattern` + ); + } + } + } + } + + return data; +} + +// Safe echo of userData: keys only, never values (may contain secrets). +export function describeUserDataKeys( + data?: Record +): string | undefined { + if (!data) return undefined; + const keys = Object.keys(data); + if (keys.length === 0) return undefined; + return keys.join(", "); +} + +// --------------------------------------------------------------------------- +// 4.6 Escrow pre-verification +// --------------------------------------------------------------------------- + +// Prints actionable errors and returns false when escrow is not ready. +export async function verifyServiceEscrow( + signer: Signer, + chainId: number, + token: string, + payee: string, // env.consumerAddress + costHuman: number, // from estimateServiceCost + durationSeconds: number +): Promise { + try { + const config = await getConfigByChainId(chainId); + if (!config?.Escrow) { + console.error( + chalk.red( + `Escrow contract address not found for chain ${chainId} in the address file.` + ) + ); + return false; + } + const escrow = new EscrowContract( + getAddress(config.Escrow), + signer, + chainId + ); + const decimals = await getTokenDecimals(signer, token); + const amountUnits = await amountToUnits( + signer, + token, + String(costHuman), + decimals + ); + const availableHuman = await unitsToAmount( + signer, + token, + amountUnits.toString(), + decimals + ); + const minLockSeconds = durationSeconds + 3600; // node getMinLockTime margin + + const validation = await escrow.verifyFundsForEscrowPayment( + token, + payee, + availableHuman, + amountUnits.toString(), + String(minLockSeconds), + "10" + ); + + if (validation.isValid === false) { + console.error(chalk.red(`Escrow check failed: ${validation.message}`)); + console.error( + chalk.yellow( + ` → deposit funds: npm run cli depositEscrow ${token} \n` + + ` → authorize node: npm run cli authorizeEscrow ${token} ${payee} \n` + + ` (maxLockSeconds must be at least ${minLockSeconds} = duration + 3600)` + ) + ); + return false; + } + return true; + } catch (error) { + console.error(chalk.red("Error verifying escrow funds:"), error); + return false; + } +} + +// --------------------------------------------------------------------------- +// 4.7 Status polling +// --------------------------------------------------------------------------- + +export async function pollServiceStatus( + nodeUrl: string, + signer: Signer, + serviceId: string, + target: ServiceStatusNumber, + timeoutMs = 600_000, + notContainerId?: string +): Promise { + const started = Date.now(); + let lastStatus: number | undefined; + + // eslint-disable-next-line no-constant-condition + while (true) { + let jobs: ServiceJob[] = []; + try { + jobs = await ProviderInstance.getServiceStatus( + nodeUrl, + signer, + serviceId + ); + } catch (error) { + // Transient errors while polling should not abort the whole wait. + console.log( + chalk.yellow( + ` (temporary error fetching status: ${ + (error as Error)?.message ?? error + })` + ) + ); + } + + const job = (jobs ?? []).find((j) => j.serviceId === serviceId); + if (job) { + if (job.status !== lastStatus) { + lastStatus = job.status; + console.log( + ` Status: ${colorForStatus( + job.status, + statusLabel(job.status, job.statusText) + )} (${job.status})` + ); + } + + const matchesContainer = + !notContainerId || job.containerId !== notContainerId; + + if (job.status === target && matchesContainer) { + return job; + } + + if (TERMINAL_FAILURE_STATUSES.includes(job.status)) { + throw new Error( + `Service ${serviceId} failed: ${statusLabel( + job.status, + job.statusText + )} (${job.status})` + ); + } + } + + if (Date.now() - started > timeoutMs) { + throw new Error( + `Timed out after ${Math.round( + timeoutMs / 1000 + )}s waiting for service ${serviceId} to reach ${statusLabel(target)}` + ); + } + + await new Promise((resolve) => setTimeout(resolve, 5000)); + } +} + +// --------------------------------------------------------------------------- +// 4.8 Job pretty-printer +// --------------------------------------------------------------------------- + +// Safe ISO expiry rendering: never throws on undefined/zero/invalid values. +export function formatExpiry(ms?: number): string { + return typeof ms === "number" && ms > 0 ? new Date(ms).toISOString() : "n/a"; +} + +function relativeTime(ms: number): string { + const diff = ms - Date.now(); + const abs = Math.abs(diff); + const mins = Math.round(abs / 60000); + if (mins < 60) return diff >= 0 ? `in ${mins}m` : `${mins}m ago`; + const hours = Math.floor(mins / 60); + const rem = mins % 60; + const label = `${hours}h${rem ? ` ${rem}m` : ""}`; + return diff >= 0 ? `in ${label}` : `${label} ago`; +} + +export function printServiceJob( + job: ServiceJob, + opts?: { verbose?: boolean } +): void { + const header = colorForStatus( + job.status, + statusLabel(job.status, job.statusText) + ); + console.log(`\nService ${chalk.bold(job.serviceId)} [${header}]`); + console.log( + ` environment: ${job.environment} owner: ${job.owner}` + ); + + const imageSpec = job.tag + ? `${job.image}:${job.tag}` + : job.checksum + ? `${job.image}@${job.checksum}` + : job.image; + console.log(` image: ${imageSpec}`); + + const expires = + typeof job.expiresAt === "number" && job.expiresAt > 0 + ? `${formatExpiry(job.expiresAt)} (${relativeTime(job.expiresAt)})` + : "n/a"; + console.log(` created: ${job.dateCreated} expires: ${expires}`); + + if (job.endpoints?.length) { + console.log(" endpoints:"); + for (const ep of job.endpoints) { + console.log( + ` → ${chalk.green(ep.url)} (container port ${ep.containerPort})` + ); + } + } else { + console.log(" endpoints: (not yet assigned — poll getServiceStatus)"); + } + + const p = job.payment ?? {}; + const paymentBits = [ + p.cost !== undefined ? `cost ${p.cost}` : null, + p.token ? `token ${p.token}` : null, + p.lockTx ? `lockTx ${p.lockTx}` : null, + p.claimTx ? `claimTx ${p.claimTx}` : null, + ].filter(Boolean); + const extendCount = job.extendPayments?.length ?? 0; + console.log( + ` payment: ${paymentBits.join(" ") || "n/a"}${ + extendCount ? ` extends: ${extendCount}` : "" + }` + ); + + if (opts?.verbose) { + console.log(util.inspect(job, false, null, true)); + } +} diff --git a/src/warnings.ts b/src/warnings.ts new file mode 100644 index 0000000..f7128bd --- /dev/null +++ b/src/warnings.ts @@ -0,0 +1,29 @@ +// Silence noisy Node process warnings emitted by transitive dependencies — +// notably punycode's `DeprecationWarning` (DEP0040) and the Ed25519 Web Crypto +// `ExperimentalWarning` — which fire asynchronously and would otherwise scroll +// the interactive REPL prompt out of view. Only these two low-signal categories +// are dropped; every other warning is still printed (in Node's default format). +// +// Every process warning — however it is emitted — is ultimately dispatched +// through the `'warning'` event, whose default listener is what prints it. So we +// remove that default listener and install our own filtering one. (Overriding +// `process.emitWarning` is not enough: core emits some warnings via a reference +// captured during bootstrap, before user code runs.) +// +// This module is imported FIRST in `src/index.ts`. ESM evaluates a module's +// imports in source order before its own body, so a side-effecting import placed +// before the others installs this hook before the dependency modules that +// trigger the warnings are evaluated. + +const SILENCED = new Set(["DeprecationWarning", "ExperimentalWarning"]); + +process.removeAllListeners("warning"); + +process.on("warning", (warning: Error & { code?: string }) => { + if (SILENCED.has(warning.name)) return; + // Reproduce Node's default one-line format for everything else. + const code = warning.code ? ` [${warning.code}]` : ""; + console.error( + `(node:${process.pid})${code} ${warning.stack ?? `${warning.name}: ${warning.message}`}` + ); +}); diff --git a/test/http.test.ts b/test/http.test.ts index d131683..0016cf9 100644 --- a/test/http.test.ts +++ b/test/http.test.ts @@ -9,7 +9,6 @@ describe('Ocean Node Root Endpoint', () => { expect(response.status).to.equal(200); expect(responseBody).to.have.property('chainIds'); expect(responseBody).to.have.property('providerAddress'); - expect(responseBody).to.have.property('serviceEndpoints'); expect(responseBody).to.have.property('software'); expect(responseBody).to.have.property('version'); diff --git a/test/replMenu.test.ts b/test/replMenu.test.ts index 51b6694..28dcf57 100644 --- a/test/replMenu.test.ts +++ b/test/replMenu.test.ts @@ -1,53 +1,5 @@ import { expect } from "chai"; -import { spawn } from "child_process"; -import path from "path"; -import { dirname } from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const projectRoot = path.resolve(__dirname, ".."); - -// Recurring prompt string emitted by the REPL (keep in sync with src/index.ts). -const PROMPT = "Enter command ('exit' | 'quit' or CTRL-C to terminate):\n"; - -/** - * Drive the interactive REPL (menu mode) with piped stdin. - * - * These tests are infra-free: PRIVATE_KEY/RPC/NODE_URL point at an unreachable - * port, so a command that actually parses and runs surfaces a "Command error" - * (connection refused) while a command that is dropped or rejected at parse time - * does not. AVOID_LOOP_RUN is left unset so the process enters the REPL loop. - */ -function runRepl( - inputLines: string[], - extraArgs: string[] = [] -): Promise<{ output: string; code: number | null }> { - return new Promise((resolve, reject) => { - const env = { ...process.env }; - delete env.AVOID_LOOP_RUN; - env.PRIVATE_KEY = - "0x1d751ded5a32226054cd2e71261039b65afb9ee1c746d055dd699b1150a5befc"; - env.RPC = "http://127.0.0.1:1"; - env.NODE_URL = "http://127.0.0.1:1"; - - const child = spawn("npx", ["tsx", "src/index.ts", ...extraArgs], { - cwd: projectRoot, - env, - }); - - let output = ""; - child.stdout.on("data", (d) => (output += d.toString())); - child.stderr.on("data", (d) => (output += d.toString())); - child.on("error", reject); - child.on("close", (code) => resolve({ output, code })); - - for (const line of inputLines) { - child.stdin.write(line + "\n"); - } - child.stdin.end(); - }); -} +import { REPL_PROMPT as PROMPT, runRepl } from "./util.js"; describe("Ocean CLI interactive menu (REPL)", function () { this.timeout(60000); diff --git a/test/serviceFlow.test.ts b/test/serviceFlow.test.ts new file mode 100644 index 0000000..b0c9de6 --- /dev/null +++ b/test/serviceFlow.test.ts @@ -0,0 +1,267 @@ +import { expect } from "chai"; +import fs from "fs"; +import { homedir } from "os"; +import { runCommand } from "./util.js"; + +/** + * Service-on-Demand (Service-on-Demand) end-to-end flow. + * + * Requires a running Ocean stack (barge) whose node has the services feature + * enabled (ocean-node v4+ / PR #1402): at least one service template and a + * compute environment with `features.services !== false`. On a stock node with + * no services support the whole lifecycle skips cleanly (`skipLifecycle`). + * + * Deviation from the plan: rather than launching a heavy model template (the + * bundled templates download multi-GB models from Hugging Face — minutes long + * and flaky in CI), the lifecycle uses a tiny, cache-friendly custom image + * (nginx-unprivileged:alpine, listening on the high port 8080 — services can't + * bind ports < 1024 because of `CapDrop ALL`). `getServiceTemplates` is still + * asserted separately so template parsing is covered. + */ +describe("Ocean CLI Service-on-Demand", function () { + this.timeout(600000); + + process.env.AVOID_LOOP_RUN = "true"; + + // Lightweight image split into image + tag (the node builds `image:tag`; a tag + // baked into the image field yields a Docker "invalid reference format"). + const IMAGE = "nginxinc/nginx-unprivileged"; + const TAG = "alpine"; + const CONTAINER_PORT = 8080; + // Bounded by the escrow authorization ceiling, NOT by what the node would allow. + // The node locks funds for getMinLockTime(duration) = duration + claimDurationTimeout + // (3600 by default) and refuses to start when that exceeds the authorization's + // maxLockSeconds ("No valid escrow auths found(maxLockSeconds too low)"). + // paidComputeFlow runs before this suite and, via ocean.js + // verifyFundsForEscrowPayment, auto-creates the authorization for this same + // (payer, token, node) at maxJobDuration + queueMaxWaitTime + 3600 = 4500s — and + // ocean.js sends no tx for a payee that is already authorized, so the + // authorizeEscrow call below CANNOT raise that ceiling. Keep duration + 3600 + // comfortably under it. + const START_DURATION = 600; // seconds + const EXTEND_DURATION = 60; // seconds + + let skipLifecycle = false; + let template: any; + let servicesEnv: any; + let oceanToken: string; + let serviceId: string; + + const getAddresses = () => { + const data = JSON.parse( + fs.readFileSync( + process.env.ADDRESS_FILE || + `${homedir()}/.ocean/ocean-contracts/artifacts/address.json`, + "utf8" + ) + ); + return data.development; + }; + + const parseTrailingArray = (output: string, prefix: string): any[] | null => { + const re = new RegExp(`${prefix}\\s*(\\[[\\s\\S]*\\])`); + const m = output.match(re); + if (!m) return null; + try { + return JSON.parse(m[1]); + } catch { + return null; + } + }; + + before(function () { + process.env.PRIVATE_KEY = + process.env.PRIVATE_KEY || + "0x1d751ded5a32226054cd2e71261039b65afb9ee1c746d055dd699b1150a5befc"; + process.env.RPC = process.env.RPC || "http://localhost:8545"; + process.env.NODE_URL = process.env.NODE_URL || "http://localhost:8001"; + process.env.ADDRESS_FILE = + process.env.ADDRESS_FILE || + `${homedir()}/.ocean/ocean-contracts/artifacts/address.json`; + oceanToken = getAddresses().Ocean; + }); + + it("lists service templates with 'getServiceTemplates'", async function () { + const output = await runCommand(`npm run cli getServiceTemplates`); + + if (output.includes("no Service-on-Demand templates")) { + console.log("Node has no service templates — skipping lifecycle."); + skipLifecycle = true; + this.skip(); + return; + } + + const templates = parseTrailingArray(output, "Service templates:"); + expect(templates, "could not parse 'Service templates:' output").to.be.an( + "array" + ).that.is.not.empty; + template = templates[0]; + expect(template).to.have.property("id").that.is.a("string"); + // Operator secrets must never leak: only env-var KEYS are exposed. + expect(template).to.not.have.property("envVars"); + expect(output).to.not.match(/JUPYTER_TOKEN\s*[:=]/i); + }); + + it("finds a compute environment with services enabled", async function () { + if (skipLifecycle) this.skip(); + const output = await runCommand(`npm run cli getComputeEnvironments`); + const envs = parseTrailingArray(output, "Existing compute environments:"); + expect(envs, "could not parse compute environments").to.be.an("array").that + .is.not.empty; + servicesEnv = (envs || []).find((e: any) => e?.features?.services !== false); + if (!servicesEnv) { + console.log("No services-enabled environment — skipping lifecycle."); + skipLifecycle = true; + this.skip(); + return; + } + expect(servicesEnv).to.have.property("consumerAddress").that.is.a("string"); + console.log(`Using services env: ${servicesEnv.id}`); + }); + + it("funds escrow (deposit + authorize the env consumer)", async function () { + if (skipLifecycle) this.skip(); + + // Best-effort mint — the well-known key may not be the token minter, in + // which case the account is expected to be pre-funded on barge. + try { + await runCommand(`npm run cli mintOcean`); + } catch { + /* tolerate: account may already hold Ocean */ + } + + const deposit = await runCommand( + `npm run cli depositEscrow ${oceanToken} 500` + ); + expect(deposit.toLowerCase()).to.match(/deposit/); + + // Authorize the env's consumerAddress as payee. maxLockSeconds must exceed + // duration + 3600; maxLockCounts covers start + extends. This is a no-op when + // an authorization already exists (ocean.js sends no tx for a known payee), so + // the values below are a floor for a fresh chain, not a guarantee. + try { + const auth = await runCommand( + `npm run cli authorizeEscrow ${oceanToken} ${servicesEnv.consumerAddress} 500 90000 100` + ); + // "Authorization failed" also contains "authoriz" — match the outcome, not the word. + expect(auth).to.match(/Successfully authorized|already authorized/i); + } catch (e) { + console.log("authorizeEscrow non-fatal (may already be authorized):", e); + } + + // Always echo the authorization actually in force: it caps START_DURATION + // (the node needs maxLockSeconds >= duration + 3600) and every later failure + // in this suite is read against it. + const auths = await runCommand( + `npm run cli getAuthorizationsEscrow ${oceanToken} ${servicesEnv.consumerAddress}` + ); + const ceiling = auths.match(/Max lock seconds:\s*(\d+)/); + if (ceiling) { + const maxLockSeconds = Number(ceiling[1]); + expect( + maxLockSeconds, + `escrow authorization allows only ${maxLockSeconds}s of lock time; ` + + `START_DURATION ${START_DURATION}s needs ${START_DURATION + 3600}s` + ).to.be.at.least(START_DURATION + 3600); + } + }); + + it("starts a service and reaches Running with an endpoint", async function () { + if (skipLifecycle) this.skip(); + const output = await runCommand( + `npm run cli -- startService ${servicesEnv.id} ${START_DURATION} ${oceanToken} ` + + `--image ${IMAGE} --tag ${TAG} --ports ${CONTAINER_PORT} ` + + `--accept true --wait true --timeout 480` + ); + + const idMatch = output.match(/ServiceID:\s*([^\s]+)/); + expect(idMatch, "could not find 'ServiceID:' in output").to.not.be.null; + serviceId = idMatch![1]; + expect(serviceId).to.be.a("string").with.length.greaterThan(0); + + expect(output, "service never reached Running").to.match(/\[Running\]|Running \(40\)/); + expect(output, "no endpoint URL printed").to.match(/http:\/\//); + console.log(`Service running: ${serviceId}`); + }); + + it("shows the service via getServiceStatus (single + list)", async function () { + if (skipLifecycle) this.skip(); + + const single = await runCommand(`npm run cli getServiceStatus ${serviceId}`); + expect(single).to.contain(serviceId); + expect(single).to.match(/http:\/\//); + expect(single).to.not.contain("userData"); + + const all = await runCommand(`npm run cli getServiceStatus`); + expect(all).to.contain(serviceId); + }); + + it("lists the service via getServices (SERVICES_LIST) without docker spec", async function () { + if (skipLifecycle) this.skip(); + + const output = await runCommand(`npm run cli getServices`); + const jobs = parseTrailingArray(output, "Services list:"); + expect(jobs, "could not parse 'Services list:'").to.be.an("array"); + const ours = (jobs || []).find((j: any) => j.serviceId === serviceId); + expect(ours, "our service not present in getServices").to.exist; + // ServiceJobListed strips the sensitive image-spec fields. + for (const j of jobs || []) { + expect(j).to.not.have.property("dockerCmd"); + expect(j).to.not.have.property("dockerEntrypoint"); + expect(j).to.not.have.property("dockerfile"); + } + + const filtered = await runCommand(`npm run cli -- getServices --status 40`); + const running = parseTrailingArray(filtered, "Services list:"); + expect(running, "could not parse filtered 'Services list:'").to.be.an( + "array" + ); + expect( + (running || []).some((j: any) => j.serviceId === serviceId) + ).to.equal(true); + }); + + it("fetches service logs (lenient)", async function () { + if (skipLifecycle) this.skip(); + // Logs may be empty or unavailable for a freshly started container; only + // assert the command runs and produces a recognizable line. + const output = await runCommand( + `npm run cli -- serviceLogs ${serviceId} --since 10m` + ); + expect(output).to.match(/Service Logs:|No logs available/); + }); + + it("extends the service expiry with extendService", async function () { + if (skipLifecycle) this.skip(); + const output = await runCommand( + `npm run cli -- extendService ${serviceId} ${EXTEND_DURATION} --accept true` + ); + expect(output).to.match(/extended/i); + expect(output).to.match(/extendPayments:\s*[1-9]/); + }); + + it("restarts the container with restartService", async function () { + if (skipLifecycle) this.skip(); + const output = await runCommand( + `npm run cli -- restartService ${serviceId} --wait true --timeout 300` + ); + expect(output).to.match(/restarting/i); + expect(output).to.match(/\[Running\]|Running \(40\)/); + }); + + it("stops the service with stopService", async function () { + if (skipLifecycle) this.skip(); + const output = await runCommand(`npm run cli stopService ${serviceId}`); + expect(output).to.match(/Stopped \(70\)|stopped/i); + }); + + after(async function () { + // Best-effort teardown if a mid-flow failure left the service running. + if (skipLifecycle || !serviceId) return; + try { + await runCommand(`npm run cli stopService ${serviceId}`); + } catch { + /* already stopped or gone */ + } + }); +}); diff --git a/test/setNode.test.ts b/test/setNode.test.ts new file mode 100644 index 0000000..3e5d8e1 --- /dev/null +++ b/test/setNode.test.ts @@ -0,0 +1,135 @@ +import { expect } from "chai"; +import { runRepl } from "./util.js"; + +// The Ocean Node exposed by Barge over HTTP. Hardcoded (as in http.test.ts) so these +// tests behave identically on both CI transport legs: the p2p leg only changes the +// NODE_URL env, the node's HTTP interface is up either way. +const LIVE_NODE = "http://127.0.0.1:8001"; + +describe("Ocean CLI node selection", function () { + this.timeout(120000); + + describe("with no NODE_URL set (no infra needed)", function () { + it("starts anyway and says which commands are available", async function () { + const { output, code } = await runRepl(["exit"], { + env: { NODE_URL: undefined }, + }); + expect(output).to.contain("No Ocean Node configured"); + expect(output).to.contain("setNode"); + expect(code).to.equal(0); + }); + + it("refuses a command that needs a node, without running it", async function () { + const { output } = await runRepl(["getComputeEnvironments", "exit"], { + env: { NODE_URL: undefined }, + }); + expect(output).to.contain("No Ocean Node set"); + // The refusal must come from the gate, not from the command being unknown. + expect(output).to.not.contain("Invalid option"); + // "Using Ocean Node URL" is logged by the Commands constructor, so the action + // body clearly never ran. (Weak on its own — runRepl's RPC is unreachable, so a + // command that got past the gate would die before that log too.) + expect(output).to.not.contain("Using Ocean Node URL"); + }); + + it("still allows help and getNode", async function () { + const { output } = await runRepl(["help", "getNode", "exit"], { + env: { NODE_URL: undefined }, + }); + expect(output).to.contain("Usage: ocean-cli"); + // Both new commands must be discoverable from the menu. + expect(output).to.contain("setNode"); + expect(output).to.contain("getNode"); + expect(output).to.contain("No Ocean Node set"); + }); + + it("keeps the CLI node-less when setNode cannot reach the node", async function () { + const { output } = await runRepl( + ["setNode http://127.0.0.1:1", "getComputeEnvironments", "exit"], + { env: { NODE_URL: undefined } } + ); + expect(output).to.contain("Still no node set"); + // The gate is still closed: no half-switch. + expect(output).to.contain("No Ocean Node set"); + expect(output).to.not.contain("Using Ocean Node URL"); + }); + + it("accepts the useNode alias", async function () { + const { output } = await runRepl(["useNode http://127.0.0.1:1", "exit"], { + env: { NODE_URL: undefined }, + }); + expect(output).to.not.contain("Invalid option"); + expect(output).to.contain("Cannot reach"); + }); + }); + + describe("with an unreachable NODE_URL set (no infra needed)", function () { + it("does not gate commands — they run and fail at the network", async function () { + const { output } = await runRepl(["getComputeEnvironments", "exit"]); + expect(output).to.not.contain("No Ocean Node set"); + expect(output).to.contain("Command error"); + }); + }); + + describe("libp2p lifecycle (no infra needed)", function () { + it("still exits when libp2p has been started", async function () { + // Regression guard: libp2p is started eagerly in loop mode, and a started + // libp2p node keeps the event loop alive — even after a clean stop() it leaves + // a MessagePort behind. Without the explicit shutdown+exit in index.ts the CLI + // hangs forever here instead of returning to the shell. + const { code } = await runRepl(["exit"], { + env: { DISABLE_P2P: undefined }, + }); + expect(code).to.equal(0); + }); + + it("does not start libp2p for a one-shot HTTP run", async function () { + // One-shot has no later command to warm up for, so paying the libp2p + // startup/shutdown cost would only slow every scripted invocation down. + const { output } = await runRepl([], { + extraArgs: ["getNode"], + env: { DISABLE_P2P: undefined, AVOID_LOOP_RUN: "true" }, + }); + expect(output).to.not.contain("Starting libp2p"); + }); + }); + + describe("against a running node (requires Barge)", function () { + it("reports the startup node and its version", async function () { + const { output } = await runRepl(["getNode", "exit"], { + env: { NODE_URL: LIVE_NODE }, + }); + expect(output).to.contain(`Current Ocean Node: ${LIVE_NODE}`); + expect(output).to.contain("Version:"); + }); + + it("switches from no node to a live node, opening the gate", async function () { + const { output } = await runRepl( + [`setNode ${LIVE_NODE}`, "getNode", "getComputeEnvironments", "exit"], + { env: { NODE_URL: undefined } } + ); + expect(output).to.contain(`Using node: ${LIVE_NODE}`); + expect(output).to.contain(`Current Ocean Node: ${LIVE_NODE}`); + // The gate opened: the command that follows the switch is no longer refused. + // (It still fails further on — runRepl points RPC at an unreachable port — so + // this cannot assert anything the action itself would print.) + expect(output).to.not.contain("No Ocean Node set"); + }); + + it("recognises a switch to the node already in use", async function () { + const { output } = await runRepl([`setNode ${LIVE_NODE}`, "exit"], { + env: { NODE_URL: LIVE_NODE }, + }); + expect(output).to.contain("already the active one"); + }); + + it("keeps the current node when the new one is unreachable", async function () { + const { output } = await runRepl( + ["setNode http://127.0.0.1:9999", "getNode", "exit"], + { env: { NODE_URL: LIVE_NODE } } + ); + expect(output).to.contain(`Keeping current node: ${LIVE_NODE}`); + expect(output).to.contain(`Current Ocean Node: ${LIVE_NODE}`); + }); + }); +}); diff --git a/test/util.ts b/test/util.ts index d830ecb..b8e942f 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,6 +1,7 @@ -import { exec } from "child_process"; +import { exec, spawn } from "child_process"; import path from "path"; import util from "util"; +import { config as chaiConfig } from "chai"; import { dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -13,11 +14,29 @@ export const __dirname = dirname(__filename) export const projectRoot = path.resolve(__dirname, ".."); + +// Never truncate assertion diffs: CLI output is long and a 40-char truncation +// ("expected '\n> @oceanprotocol/cli@...' to match /extended/i") hides the very +// text a failure needs to be diagnosed from a CI log. +chaiConfig.truncateThreshold = 0; + +/** + * The CLI prints every failure with console.error (stderr) and only successes + * with console.log (stdout). runCommand returns stdout — so a command that + * exits 0 after printing an error would otherwise look like silent, empty + * output. Always echo stderr so the reason is in the log. + */ +const logStderr = (stderr?: string) => { + if (stderr && stderr.trim().length > 0) { + console.error(`[STDERR]:\n${stderr}`); + } +}; export const runCommand = async (command: string): Promise => { console.log(`\n[CMD]: ${command}`); try { - const { stdout } = await execPromise(command, { cwd: projectRoot }); + const { stdout, stderr } = await execPromise(command, { cwd: projectRoot }); console.log(`[OUTPUT]:\n${stdout}`); + logStderr(stderr); return stdout; } catch (error: any) { console.error(`[ERROR]:\n${error.stderr || error.message}`); @@ -31,14 +50,76 @@ export const runCommandAs = async ( ): Promise => { console.log(`\n[CMD as ${privateKey.slice(0, 6)}…]: ${command}`); try { - const { stdout } = await execPromise(command, { + const { stdout, stderr } = await execPromise(command, { cwd: projectRoot, env: { ...process.env, PRIVATE_KEY: privateKey }, }); console.log(`[OUTPUT]:\n${stdout}`); + logStderr(stderr); return stdout; } catch (error: any) { console.error(`[ERROR]:\n${error.stderr || error.message}`); throw error; } +}; + +/** Recurring prompt string emitted by the REPL (keep in sync with src/index.ts). */ +export const REPL_PROMPT = + "Enter command ('exit' | 'quit' | ESC or CTRL-C to terminate):\n"; + +export interface RunReplOptions { + /** Extra argv for the initial command run before the loop starts. */ + extraArgs?: string[]; + /** + * Env overrides applied on top of the defaults. A key set to undefined is + * deleted, which is how a test starts the CLI with no NODE_URL at all. + */ + env?: Record; +} + +/** + * Drive the interactive REPL (menu mode) with piped stdin. + * + * The defaults are infra-free: PRIVATE_KEY/RPC/NODE_URL point at an unreachable + * port, so a command that actually parses and runs surfaces a "Command error" + * (connection refused) while a command that is dropped, rejected at parse time or + * refused by the node gate does not. AVOID_LOOP_RUN is unset so the process enters + * the REPL loop. + */ +export const runRepl = ( + inputLines: string[], + options: RunReplOptions = {} +): Promise<{ output: string; code: number | null }> => { + return new Promise((resolve, reject) => { + const env: Record = { ...process.env }; + delete env.AVOID_LOOP_RUN; + env.PRIVATE_KEY = + "0x1d751ded5a32226054cd2e71261039b65afb9ee1c746d055dd699b1150a5befc"; + env.RPC = "http://127.0.0.1:1"; + env.NODE_URL = "http://127.0.0.1:1"; + // These tests must not dial the public Ocean bootstrap nodes. + env.DISABLE_P2P = "true"; + + for (const [key, value] of Object.entries(options.env || {})) { + if (value === undefined) delete env[key]; + else env[key] = value; + } + + const child = spawn( + "npx", + ["tsx", "src/index.ts", ...(options.extraArgs || [])], + { cwd: projectRoot, env } + ); + + let output = ""; + child.stdout.on("data", (d) => (output += d.toString())); + child.stderr.on("data", (d) => (output += d.toString())); + child.on("error", reject); + child.on("close", (code) => resolve({ output, code })); + + for (const line of inputLines) { + child.stdin.write(line + "\n"); + } + child.stdin.end(); + }); }; \ No newline at end of file