diff --git a/.babelrc b/.babelrc deleted file mode 100644 index acffdcf..0000000 --- a/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "presets": [ - "@babel/preset-env" - ] -} diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..2044a32 --- /dev/null +++ b/.env.template @@ -0,0 +1,31 @@ +# Copy to `.env` and fill in as needed. Real environment variables take precedence. +# +# Build-time: this file is loaded by `npm run build` (tsdown) and baked into the bundle. +# Runtime: the CLI does NOT read this file when it runs; set those vars in your shell. + +# --- Build-time --- + +# Amplitude API key baked into release builds for anonymous telemetry. +# Leave empty for local builds (telemetry stays off). +KONTENT_CLI_AMPLITUDE_API_KEY= + +# Base Kontent.ai domain; all endpoint URLs derive from it (app., manage.). +# Baked into the build from here; defaults to kontent.ai (production). Use +# devkontentmasters.com for dev. A runtime KONTENT_URL in your shell overrides it. +KONTENT_URL=devkontentmasters.com + +# Auth0 tenant baked into the build; defaults here target the QA/dev tenant. +# A runtime KONTENT_AUTH0_* var in your shell overrides each. The requested +# scope is not configurable (fixed requirement of the CLI). +KONTENT_AUTH0_DOMAIN=login.devkontentmasters.com +KONTENT_AUTH0_CLIENT_ID= +KONTENT_AUTH0_AUDIENCE=https://app.kenticocloud.com/ + +# --- Runtime (export in your shell; shown here for reference) --- + +# Telemetry opt-out (set to 1/true to disable). DO_NOT_TRACK is the standard equivalent. +KONTENT_DO_NOT_TRACK= +DO_NOT_TRACK= + +# Verbose telemetry logging for debugging. +KONTENT_TELEMETRY_DEBUG= diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9d14463..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,156 +0,0 @@ -/* -πŸ‘‹ Hi! This file was autogenerated by tslint-to-eslint-config. -https://github.com/typescript-eslint/tslint-to-eslint-config - -It represents the closest reasonable ESLint configuration to this -project's original TSLint configuration. - -We recommend eventually switching this configuration to extend from -the recommended rulesets in typescript-eslint. -https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md - -Happy linting! πŸ’– -*/ -module.exports = { - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": [ - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "root": true, - "rules": { - "@typescript-eslint/consistent-type-definitions": "error", - "@typescript-eslint/dot-notation": "off", - "@typescript-eslint/explicit-member-accessibility": [ - "off", - { - "accessibility": "explicit" - } - ], - "@typescript-eslint/member-delimiter-style": [ - "error", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "semi", - "requireLast": false - } - } - ], - "@typescript-eslint/member-ordering": "error", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-inferrable-types": [ - "off", - { - "ignoreParameters": true - } - ], - "@typescript-eslint/no-shadow": [ - "off", - { - "hoist": "all" - } - ], - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/prefer-function-type": "error", - "@typescript-eslint/semi": [ - "error" - ], - "@typescript-eslint/type-annotation-spacing": "error", - "@typescript-eslint/unified-signatures": "error", - "brace-style": [ - "error", - "1tbs" - ], - "curly": "error", - "dot-notation": "off", - "eol-last": "error", - "eqeqeq": [ - "error", - "smart" - ], - "guard-for-in": "error", - "id-denylist": "off", - "id-match": "off", - "indent": "off", - "max-len": [ - "error", - { - "code": 250 - } - ], - "no-bitwise": "error", - "no-caller": "error", - "no-console": [ - "error", - { - "allow": [ - "log", - "warn", - "dir", - "timeLog", - "assert", - "clear", - "count", - "countReset", - "group", - "groupEnd", - "table", - "dirxml", - "error", - "groupCollapsed", - "Console", - "profile", - "profileEnd", - "timeStamp", - "context" - ] - } - ], - "no-debugger": "error", - "no-empty": "off", - "no-empty-function": "off", - "no-eval": "error", - "no-fallthrough": "error", - "no-new-wrappers": "error", - "no-redeclare": "error", - "no-restricted-imports": "error", - "no-shadow": "off", - "no-throw-literal": "error", - "no-trailing-spaces": "error", - "no-underscore-dangle": "off", - "no-unused-expressions": "off", - "no-unused-labels": "error", - "no-var": "error", - "prefer-const": "error", - "quotes": "off", - "radix": "error", - "semi": "off", - "spaced-comment": [ - "error", - "always", - { - "markers": [ - "/" - ] - } - ], - "no-multi-spaces": ["error"], - "no-irregular-whitespace": ["error"] - } -}; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0c49c0e..f3bfa35 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1 @@ -# Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths. -# See https://help.github.com/articles/about-code-owners/ - -* @IvanKiral @winklertomas @kontent-ai/javascript-maintainers +* @IvanKiral @kontent-ai/developer-relations \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2d7de4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +### Brief bug description + +What went wrong? + +### Repro steps + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +### Expected behavior + +What is the correct behavior? + +### Test environment + + - Platform/OS: [e.g. .NET Core 2.1, iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +### Additional context + +Add any other context about the problem here. + +### Screenshots + +Add links to screenshots, if possible. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5311faf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +### Motivation + +Why is this feature required? What problems does it solve? + +### Proposed solution + +An ideal solution for the above problems. + +### Additional context + +Add any other context, screenshots, or reference links about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..83e5396 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +ο»Ώ--- +name: Question +about: Ask a question + +--- + +### Question + +What do you want to ask? + +### Reference + +* URL diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md new file mode 100644 index 0000000..9a3eca1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -0,0 +1,17 @@ +--- +name: Spike +about: Suggest an analysis of a problem + +--- + +### Expected result + +What do we want to explore and why? Which questions do we want to answer with this spike? + +### Additional context + +Add any other context or guidelines here. + +### Resources + +* URL diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6659752 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +### Motivation + +Which issue does this fix? Fixes #`issue number` + +If no issue exists, what is the fix or new feature? Were there any reasons to fix/implement things that are not obvious? + +### Checklist + +- [ ] Code follows coding conventions held in this repo +- [ ] Automated tests have been added +- [ ] Tests are passing +- [ ] Docs have been updated (if applicable) +- [ ] Temporary settings (e.g. variables used during development and testing) have been reverted to defaults + +### How to test + +If manual testing is required, what are the steps? diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..29ef931 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Setup pnpm + uses: pnpm/action-setup@v5 + - name: Use Node.js from .nvmrc file + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Typecheck + run: pnpm typecheck + - name: ESLint + run: pnpm lint + - name: Biome + run: pnpm biome:check + - name: Build + run: pnpm build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index c6ed749..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,28 +0,0 @@ -on: - release: - types: [published] - -name: publish-to-npm -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version-file: '.nvmrc' - registry-url: 'https://registry.npmjs.org' - - run: npm install - - run: npm run lint - - run: npm run build - - run: npm publish - if: ${{!github.event.release.prerelease}} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }} - - run: npm publish --tag prerelease - if: ${{github.event.release.prerelease}} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }} - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 571d297..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Test -on: [pull_request] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version-file: '.nvmrc' - - run: npm i - - run: npm run lint - - run: npm run build - - run: npm run test:coverage - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index ee74dd9..926851d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ -/.idea/ -/node_modules/ -/lib/ -/Migrations/*.js -Migrations/.environments.json -Migrations/status.json -/package-lock.json - -# Generated by tests -status.json -coverage \ No newline at end of file +node_modules/ +dist/ +coverage/ +.DS_Store +*.log +.env + +# Local sample-app clones created by `kontent project bootstrap` +my-kickstart/ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index d24fdfc..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 867d643..0000000 --- a/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -* -!lib/** -!.npmignore -!package.json -!README.md -!LICENSE diff --git a/.nvmrc b/.nvmrc index 1a2f5bd..b009dfb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* \ No newline at end of file +lts/* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a33e70b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +Guidance for agents working in this repo. `kontent-cli` is the Kontent.ai command-line interface (`kontent` bin β†’ `src/index.ts`). ESM, TypeScript, pnpm. + +**Keep this file current, concise, and token-aware.** When a change alters a documented architecture/convention/pattern, update CLAUDE.md in the same change β€” but prune stale or low-value lines rather than letting it grow. Always check before halting whether anything you did makes a statement here stale. + +## Before halting + +Run and pass these (same gate as CI, in order): + +``` +pnpm typecheck && pnpm lint && pnpm biome:check && pnpm test +``` + +Autofix is available: `pnpm lint:fix`, `pnpm biome:fix`. Build with `pnpm build` (tsdown). Node version is `.nvmrc` (`lts/*`). Always use pnpm, never npm/yarn. + +## Architecture + +Three layers, dependencies point downward only (`commands β†’ core β†’ lib`): + +- `src/index.ts` β€” composition root. Folds each command's `register` over yargs via `reduce`, wires shared `deps` (telemetry). +- `src/commands/**` β€” yargs wiring + presentation only. Register the command, call core, format output, log, set `process.exitCode`, fire the telemetry tracker. No business logic. +- `src/core/**` β€” orchestration of business logic. Returns `Result`/`Option`; never writes to the console directly (logs only through passed `LogOptions`). **Exception:** interactive commands may drive their own terminal UI from core β€” e.g. `src/core/project/bootstrap.ts` uses `@clack/prompts` (spinners, `confirm`/`select`, notes) directly because the flow is inherently interactive. Keep non-interactive core free of direct console writes. +- `src/lib/**` β€” reusable primitives: `auth/`, `iapi/`, `mapi/`, `config/`, `telemetry/`, plus `result.ts` and `option.ts`. + +Adding a command: export a `register: RegisterCommand` (see `src/commands/login/login.ts`), then add its import to the `register` array in the parent command or `src/index.ts`. + +### API clients + +- `iapi` (`src/lib/iapi`) β€” internal Kontent.ai API; hand-rolled client, one file per endpoint, over `@kontent-ai/core-sdk`. Endpoint validators (the `schema` field) must be **`zod/mini`** (`import * as z from "zod/mini"`) β€” classic `zod` won't infer the payload. +- `mapi` (`src/lib/mapi`) β€” public Management API via `@kontent-ai/management-sdk`. +- `@kontent-ai/core-sdk` β€” shared HTTP/SDK layer both clients build on. + +**Commands build clients; core receives them.** The command builds the `iapiClient`/`mapiClient` and passes them into core (e.g. `performBootstrap(params, { iapiClient, mapiClient })`); core never constructs clients itself. Auth failure is handled in the command, not surfaced as a core `Result` error. + +## Conventions + +- **Functional, not OOP.** No classes. Modules of small, composable, pure functions. Compose with `reduce`, `match` (`ts-pattern`), and the `Result`/`Option` combinators. +- **Errors are values.** Don't throw across layers. Use `Result` (`src/lib/result.ts`) and `Option` (`src/lib/option.ts`); convert thrown errors at the boundary with `tryAsync`/`fromThrowable`. +- **Boolean names** start with a helper verb: is/has/can/should/was (e.g. `isAlreadyAuthenticated`, `shouldForceRefresh`). Applies to params, locals, fields, and props. +- **`const` over `let`** unless reassignment is genuinely required. +- **No `return` on the same line as its condition** β€” put the guard's body on its own line. +- **Prefer `readonly`** types and `ReadonlyArray` for inputs. +- **ESM import extensions:** relative imports must end in `.js` (biome enforces `useImportExtensions`). +- **No redundant wrappers.** Don't add a function that only forwards to another; reuse existing helpers instead of duplicating logic. +- **Comments only for non-obvious "why".** No restating-the-code comments. No emojis anywhere. +- **No barrel files** except a deliberate public API. + +## Testing + +Vitest; `test/integration/` for integration tests, `test/helpers/` for shared helpers. Run `pnpm test`. Inject fakes into core instead of real I/O β€” for iapi reuse `test/helpers/iapiTestClient.ts` (real client over core-sdk's `HttpAdapter` seam, declarative routes). + +## Telemetry + +Amplitude-based, see `TELEMETRY.md`. New `KONTENT_*` env vars need a hidden yargs option registered in `src/index.ts` β€” `.strict()` + `.env("KONTENT")` rejects unknown env vars otherwise. + +Event names and the custom event-property keys we set are kebab-case (`cli__some-command`, `error-code`, `sample-project-type`); single words stay bare (`outcome`). Amplitude's built-in fields (`device_id`, `user_id`, `platform`, `app_version`, `os_name`, `os_version`) are the exception and keep `snake_case`. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2bbf8c9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at devrel@kontent.ai. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1c1ba4a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Ways to contribute + + +There are many different ways in which you can contribute. One of the easiest ways is simply to use our software and provide us with your feedback through the right channel. You can also help us improve the open-source projects by submitting pull requests with code and documentation changes. + +## Where to get support +Please note that **level of provided support is always determined by the LICENSE** of a given open-source project. Also, always make sure you use the **[latest version](../../releases)** of any given OS project. We can't provide any help for older versions. We don't want to make things complicated so we try to take the same approach in all our repositories. + +### I found a bug in a Kontent.ai open-source project + + +Sorry to hear that. Just log a new [GitHub issue](../../issues) and someone will take a look at it. Remember, the more information you provide, the easier it will be to fix the issue. If you feel like it, you can also fix the bug on your own and submit a new pull request. + +### I need help with using the projects and/or coding + + +To get help with coding and structuring your projects, use [StackOverflow](https://stackoverflow.com/) and tag your questions with [`kontent-ai`](https://stackoverflow.com/questions/tagged/kontent-ai) tag. + +Our team members and the community monitor these channels on a regular basis. + +### I want to report a security bug + + +Security issues and bugs should be reported privately, via email, to Kontent.ai Security Team [security@kontent.ai](mailto:security@kontent.ai). For more details, check the [Security policy](SECURITY.md). + +### I have an idea for a new feature (or feedback on existing functionality) + + +Everybody loves new features! You can submit a new [feature request](../../issues) or you can code it on your own and [send us a pull request](#submitting-pull-requests). In either case, don't forget to mention what's the use case and what's the expected output. + + +## Submitting pull requests + + +Unless you're fixing a typo, it's usually a good idea to discuss the feature before you submit a pull request with code changes, so let's start with submitting a new [GitHub issue](../../issues) and discussing the whether it fits the vision of a given project. +You might also read these two blogs posts on contributing code: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza and [Don't "Push" Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. Note that all code submissions will be rigorously reviewed and tested by Kontent.ai maintainer teams, and only those that meet an high bar for both quality and design/roadmap appropriateness will be merged into the source. + + +### Example - process of contribution +If not stated otherwise, we use [feature branch workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). + +To start with coding, fork the repository you want to contribute to, create a new branch, and start coding. Once the functionality is [done](#Definition-of-Done), you can submit a [pull request](https://help.github.com/articles/about-pull-requests/). + +### Definition of Done + + +- New/fixed code is covered with tests +- CI can build the code +- All tests are pass +- New version number follows [semantic versioning](https://semver.org/) +- Coding style (spaces, indentation) is in line with the rest of the code in a given repository +- Documentation is updated (e.g. code examples in README, Wiki pages, etc.) +- All `public` members are documented (using XML doc, phpdoc, etc.) +- Code doesn't contain any secrets (private keys, etc.) +- Commit messages are clear. Please read these articles: [Writing good commit messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages), [A Note About Git Commit Messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), [On commit messages](https://who-t.blogspot.com/2009/12/on-commit-messages.html) + + +### Feedback + + +Your pull request will now go through extensive checks by the subject matter experts on our team. Please be patient. Update your pull request according to feedback until it is approved by one of Kontent.ai maintainers. After that, one of our team members may adjust the branch you merge into based on the expected release schedule. + + +## Code of Conduct + + +The Kontent.ai team is committed to fostering a welcoming community, therefore this project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). If you have any additional questions or comments, you can contact us directly at devrel@kontent.ai. diff --git a/LICENSE b/LICENSE.md similarity index 94% rename from LICENSE rename to LICENSE.md index 290435b..ef89826 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2022 Kontent s.r.o. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) [Current year here] Kontent s.r.o. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f0bbaf1..54602ac 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,83 @@ -# Kontent.ai CLI - -[![npm](https://img.shields.io/npm/v/@kontent-ai/cli.svg)](https://www.npmjs.com/package/@kontent-ai/cli) -[![Build](https://github.com/kontent-ai/cli/actions/workflows/test.yml/badge.svg)](https://github.com/kontent-ai/cli/actions/workflows/test.yml) - -The Kontent.ai CLI helps you when you need to change content models within your [Kontent.ai](https://kontent.ai/) environments and migrate existing content to match the changes. The CLI provides you with guidance on how to write and run migration scripts. - -**_NOTE:_** The Kontent.ai CLI tool supports only Javascript files, so if you write your migrations in Typescript or any other language you have to transpile your code before running. - -- [Kontent.ai CLI](#kontentai-cli) - - [Installation](#installation) - - [🌟 Migration example](#-migration-example) - - [1. Prepare a testing environment](#1-prepare-a-testing-environment) - - [2. Prepare Kontent.ai CLI boilerplate](#2-prepare-kontentai-cli-boilerplate) - - [3. Run a migration](#3-run-a-migration) - - [4. Explore existing migrations](#4-explore-existing-migrations) - - [Usage](#usage) - - [Commands](#commands) - - [Custom implementation of reading/saving status of migrations](#custom-implementation-of-readingsaving-status-of-migrations) - - [Debugging](#debugging) - - [The vision](#the-vision) - - [Feedback \& Contribution](#feedback--contribution) - -## Installation - -The Kontent.ai CLI requires Node 10+ and npm 6+, and uses the [Kontent.ai Management SDK](https://github.com/kontent-ai/management-sdk-js) to manipulate content in your environments. - -```sh -npm install -g @kontent-ai/cli -``` - -## 🌟 Migration example - -The current version of the CLI is useful for creating and running migration templates. Let's go through creating your first migration for a Kontent.ai project. - -### 1. Prepare a testing environment - -When you need to add new features to your project and app, it's better to verify the changes in a separate non-production environment. In Kontent.ai, [clone your project](https://kontent.ai/learn/tutorials/manage-kontent/projects/clone-projects) from the list of your projects. - -### 2. Prepare Kontent.ai CLI boilerplate - -To improve the learning curve of our new CLI, we've prepared a [Kontent.ai CLI boilerplate](https://github.com/kontent-ai/migrations-boilerplate) with examples on how to use the CLI. Clone the boilerplate GitHub repository on your drive. In the next step, you'll run a migration script from the boilerplate's `Migrations` directory. - -### 3. Run a migration - -Open a command line and navigate to the root of the boilerplate folder (should be `migrations-boilerplate`) and execute the following commands: - -```sh -# Navigates to the root of the Kontent.ai CLI boilerplate folder. -cd ./migrations-boilerplate - -npm install - -# Registers an environment (a pair of two keys, a environment ID and API key used to manage the environment) for migrations. -kontent environment add --name DEV --api-key --environment-id (Use the copy of your production project from the first step) - -# Runs a specific migration. -npm run migrate 01_sample_init_createBlogType -``` - -Kontent.ai CLI supports only running JavaScript migration files so in case you want to write in TypesScript, CoffeScript or in any other language you have to transpile your code before running. -In the case of TypeScript, you may use this example from [Kontent.ai CLI boilerplate](https://github.com/kontent-ai/migrations-boilerplate/blob/master/package.json#L7) - -That's it! You've run your first Kontent.ai migration. This migration created a content type called *Blog* that contains three text elements named *Title*, *Author* and *Text*. The sample migration is written in TypeScript. - -The boilerplate is configured to transpile TypeScript migrations into plain JavaScript so that the Kontent.ai CLI can execute the migrations. Note that if you don't want to use TypeScript for your migrations, it's fine to write the migrations directly in JavaScript. - -### 4. Explore existing migrations - -You should now be able to go through the other boilerplate sample migrations. The migration scripts in the *Migrations* directory all focus on one scenario – replacing a piece of text from the *Author* text element with *Author* content items, which contain more information about the author. This way you can replace the texts within your items by more complex objects containing, for example, images and rich text. - -You can use similar approach for your own specific scenarios. For example, imagine you need to add display information to the images inserted in your articles. You may want to specify relative size or caption for each image. To do this, you would need to open each article and replace every image with a component that would contain the image and a few elements for image metadata. You'd create small migration scripts for separate parts of the scenario (such as creating a new type, updating the articles, and so on) and the migrations will take care of the process for all articles within your environment. - -## Usage - -Use the `--help` parameter to display the help section for CLI tool. - -```sh -kontent --help -``` - -Combine the `--help` parameter with a specific command to learn more about that command. - -```sh -kontent migration add --help -``` - -### Commands - -The supported commands are divided into groups according to their target, at this first version there are just to spaces "migration" and "environment" containing following commands: - -* `environment add` – Store information about the environment locally. - * The environment is defined as a named pair of values. For example, "DEV" environment can be defined as a pair of a specific environment ID and Management API key. This named pair of values is stored within your local repository in a configuration file named `.environments.json`. - * You can specify a named pair of environment ID and Management API key using these options: `--environment-id --api-key --name `. - -* `migration add --name ` – Generates a script file (in JavaScript or TypeScript) for running a migration on a [Kontent.ai](https://kontent.ai/) environment. - * The file is stored in the `Migrations` directory within the root of your repository. - * Add your migration script in the body of the `run` function using the [Kontent.ai Management SDK](https://github.com/kontent-ai/management-sdk-js) that was injected via the `apiClient` parameter. - * Add your rollback script in the body of the `rollback` function using the [Kontent.ai Management SDK](https://github.com/kontent-ai/management-sdk-js) that was injected via the `apiClient` parameter. - * To choose between JavaScript and TypeScript when generating the script file, use the `--template-type` option, such as `--template-type "javascript"`. - * The migration template contains an `order` property that is used to run a batch of migrations (range or all) in the specified order. Order can be one of the two types - `number` or `date`. - * Ordering by `number` has a higher priority. The `order` must be a unique positive integer or zero. There may be gaps between migrations, for example, the following sequence is perfectly fine 0,3,4,5,10 - * Ordering by `date` has a lower priority. To add date ordering use the switch option `-d`. The CLI will generate a new file which name consists of the date in UTC and the name you have specified. Moreover, the property `order` inside the file will be set to the Date accordingly. - * Executing all migrations will firstly migrate migrations with orders specified by number and only then migrations with order specified by date. - * By specifying range you can migrate either number-ordered migrations or date-numbered migrations. They can't be combined. - - ```typescript - // Example migration template - import {MigrationModule} from "@kontent-ai/cli"; - - const migration: MigrationModule = { - order: 1, - run: async (apiClient) => { - // TODO: Your migration code. - }, - }; - - export default migration; - ``` - -* `migration run` - Runs a migration script specified by file name (option `--name `), or runs multiple migration scripts in the order specified in the migration files (options `--all` or `--range`). - * By adding `--range` you need to add value in form of `number:number` in case of number ordering or in the format of `Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss` in case of date order. - > When using the range with dates, only the year value is mandatory and all other values are optional. It is fine to have a range specified like `T2023-01:2023-02`. It will take all migrations created in January of 2023. Notice the T at the beginning of the Date range. It helps to separate date ordering from number order. - * You can execute a migration against a specific environment (options `--environment --api-key `) or environment stored in the local configuration file (option `--environment `). - * To execute your `rollback` scripts instead of `run` scripts, use the switch option `--rollback` or shortly `-b`. - * After each run of a migration script, the CLI logs the execution into a status file. This file holds data for the next run to prevent running the same migration script more than once. You can choose to override this behavior, for example for debugging purposes, by using the `--force` parameter. - > Note: For every migration there is only one record in the status file. Calling run/rollback continuously overrides the that record with new data. - * You can choose whether you want to keep executing the migration scripts even if one migration script fails (option `--continue-on-error`) or whether you want to get additional information logged by HttpService into the console (option `--log-http-service-errors-to-console`). - -* `backup --action [backup|restore|clean]` - This command enables you to use [Kontent.ai backup manager](https://github.com/kontent-ai/backup-manager-js) - * The purpose of this tool is to backup & restore [Kontent.ai projects](https://kontent.ai/). This project uses CM API to both get & restore data. - -### Custom implementation of reading/saving status of migrations - -You might want to implement your way to store information about migrations status. For instance, you would like to save it into DB such as MongoDB, Firebase, etc,... and not use the default JSON file. Therefore, we provide you with an option to implement functions `readStatus` and `saveStatus`. To do so, create a new file called `plugins.js` at the root of your migrations project, and implement mentioned functions there. To fit into the required declarations, you can use the template below: - -```js -//plugins.js -exports.saveStatus = async (data) => {} - -exports.readStatus = async () => {} -``` -> Note: Both functions must be implemented. - -It is also possible to use Typescript. We have prepared types `SaveStatusType` and `ReadStatusType` to typesafe your functions. To create plugins in Typescript, create a file `plugins.ts` and implement your functions there. We suggest using and implementing the template below: - -```ts -//plugins.ts -import type { ReadStatusType, SaveStatusType } from "@kontent-ai/cli"; - -export const saveStatus: SaveStatusType = async (data: string) => {} - -export const readStatus: ReadStatusType = async () => {} -``` - -> Note: Don't forget to transpile `plugins.ts` into `plugins.js` otherwise your plugins will not work. - -### Debugging - -By default, we do not provide any additional logs from the HttpService. If you require these logs, you can change this behavior by using (option `--log-http-service-errors-to-console`). - -If you come across an error and you're not sure how to fix it, execute your migration script as follows and setup your debugger to the specified port. - -```sh -node --inspect .\node_modules\@kontent-ai\cli\lib\index.js migration run -n 07_sample_migration_publish -e DEV -``` - -## The vision - -* Writing migration scripts can involve a lot of repetitive work, especially when it requires getting different object types and iterating through them. That's why we've decided to continue improving the developer experience and focus on that in upcoming releases. We plan to reduce the code that you need to write to the bare minimum by providing you with a "command builder". This builder will allow you to write migrations using queries and callbacks that should be applied to every object selected by that query. For example, select content types, all items based on the types, and all variants of the items, and execute your callback function on them. - -* The tool isn't reserved only for migrations. A valid use case could also be Kontent.ai project data export and import, which could together with the possibility to clone/create/archive projects via the management API be a great way to e.g. run integration tests on the test environment that would be archived after the successful tests run. - -## Feedback & Contribution - -Check out the [contributing](./CONTRIBUTING.md) page to see the best places to file issues, start discussions, and begin contributing. We have lot of ideas on how to improve the CLI and developer experience with our product, but we'd love to hear from you so we can focus on your needs. +[![Stargazers][stars-shield]][stars-url] +[![MIT License][license-shield]][license-url] +[![Discord][discussion-shield]][discussion-url] + +# Kontent.ai CLI + +Command-line interface for [Kontent.ai](https://kontent.ai). + +> [!WARNING] +> **Under active development.** The command surface is incomplete and may change +> between releases. More commands are added incrementally. Breaking changes may +> occur. + +> [!IMPORTANT] +> **Looking for the previous CLI?** The legacy `@kontent-ai/cli` commands for +> content migrations and environment backup/restore are deprecated. That +> functionality now lives in +> [`@kontent-ai/data-ops`](https://github.com/kontent-ai/data-ops). + +## Prerequisites + +- [Node.js](https://nodejs.org) LTS or newer +- A [Kontent.ai](https://app.kontent.ai) account + +## Installation + +Run without installing: + +```sh +npx kontent-cli@latest +``` + +Or install globally to get the `kontent` command: + +```sh +npm install -g kontent-cli +kontent +``` + +Using `@latest` ensures you run the newest version. + +## Authentication + +Most commands require you to be signed in. + +```sh +kontent login +kontent logout +``` + +## Commands + +Run `kontent --help` for the full, always-current list. +Each command supports `--help` for its own options. + +## Global options + +- `--logLevel`, `-ll` β€” detail level: `none`, `standard` (default), `verbose` +- `--verbose` β€” shortcut for `--logLevel verbose` +- `--configFile` β€” path to a JSON file with CLI parameters +- `--help`, `-h` / `--version`, `-v` + +Options can also be supplied via `KONTENT_*` environment variables. + +## Telemetry + +The CLI collects anonymous usage data. See [telemetry.md](./telemetry.md) for details and opt-out. + +## Contributing + +Contributions are welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for details. + +## License + +Distributed under the MIT License. See [`LICENSE.md`](./LICENSE.md). + + +[stars-shield]: https://img.shields.io/github/stars/kontent-ai/kontent-cli.svg?style=for-the-badge +[stars-url]: https://github.com/kontent-ai/kontent-cli/stargazers +[license-shield]: https://img.shields.io/github/license/kontent-ai/kontent-cli.svg?style=for-the-badge +[license-url]: https://github.com/kontent-ai/kontent-cli/blob/main/LICENSE.md +[discussion-shield]: https://img.shields.io/discord/821885171984891914?color=%237289DA&label=Kontent%2Eai%20Discord&logo=discord&style=for-the-badge +[discussion-url]: https://discord.com/invite/SKCxwPtevJ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4c81d73 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +## Security +In [Kontent.ai](http://kontent.ai/), security has always been of great importance. Kontent.ai is committed to working with security researchers to help identify and fix vulnerabilities in our systems and services, which includes all source code repositories managed through our [GitHub organization](https://github.com/kontent-ai). + +If you believe you have found a security vulnerability in any Kontent.ai-owned repository that meets our [qualification criteria](https://kontent.ai/vulnerability-disclosure-policy/), please report it to us as described below. + +## Reporting Security Issues +**Please do not report security vulnerabilities through public GitHub issues.** + +Security issues and bugs should be reported privately, via email to [security@kontent.ai](mailto:security@kontent.ai). For secure communication, use our [PGP key](https://app.kontent.ai/pgp-key.txt). If you find multiple issues, please report them separately. We will keep you updated on the progress towards remediation of issues we accept from you, and we ask you not to disclose the issue publicly without Kontent.ai’s prior written permission. Additional information can be found in our [Vulnerability Disclosure Policy](https://kontent.ai/vulnerability-disclosure-policy/). + +If possible, please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the issue: + +* Type of issue (e.g. SQL injection, XSS, broken access control…) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages +We prefer all communications to be in English. + +## Safe harbor +Kontent.ai will not initiate a lawsuit or law enforcement investigation against you in response to reporting a vulnerability if you fully comply with our [Vulnerability Disclosure Policy](https://kontent.ai/vulnerability-disclosure-policy/). \ No newline at end of file diff --git a/TELEMETRY.md b/TELEMETRY.md new file mode 100644 index 0000000..c85dfeb --- /dev/null +++ b/TELEMETRY.md @@ -0,0 +1,130 @@ +# Telemetry + +The Kontent.ai CLI collects anonymous usage telemetry. This document explains +what is collected, what is not, how it is identified, and how to turn it off. + +## Why we collect telemetry + +Telemetry helps us understand which commands are used, how often they succeed or +fail, and how long they take, so we can prioritize fixes and improvements. +Collection is best-effort and never affects command behavior: if telemetry fails +or is disabled, commands run exactly the same. + +## Consent + +Telemetry is **opt-out**: it is on by default in release builds unless one of the +conditions in [How to disable telemetry](#how-to-disable-telemetry) applies. The +first time the CLI sends telemetry, it prints a one-time notice: + +```text +kontent-cli sends anonymous usage telemetry (command name, success/failure, +duration) to help improve it. Opt out: kontent telemetry disable +``` + +## What is collected + +Every event carries a small, fixed set of properties: + +| Property | Description | +| ------------- | ------------------------------------------------------ | +| `outcome` | `success` or `error` | +| `error-code` | A machine-readable error code (only when `outcome` is `error`) | +| `duration-ms` | Command execution time in milliseconds | +| `device_id` | Anonymous, randomly generated device identifier | +| `user_id` | Your Kontent.ai user id (only when you are logged in) | +| `platform` | Always `CLI` | +| `app_version` | CLI version | +| `os_name` | Operating system name (e.g. `darwin`, `linux`, `win32`)| +| `os_version` | Operating system release version | + +### Events + +Commands that send telemetry emit a single event named `cli__` β€” +the command and any subcommands in kebab-case (e.g. `kontent login` β†’ +`cli__login`, `kontent project sample bootstrap` β†’ `cli__project-sample-bootstrap`). +Not every command sends telemetry. + +Beyond the fixed properties above, some commands attach a few Kontent.ai resource +identifiers (GUIDs) for the resource they act on β€” e.g. bootstrap adds `project`, +`subscription`, `sample-project-type`. These are never content, credentials, or +command argument values. + +## What is NOT collected + +- Credentials of any kind: API keys, access tokens, passwords. +- The contents of your project, items, assets, or any managed content. +- Command argument values (only the fixed properties listed above are sent). +- Your email address or other personal information beyond the `user_id`. + +## Identifiers + +- **`device_id`** β€” a random UUID generated on first run and stored locally. It + identifies a machine, not a person. +- **`user_id`** β€” your Kontent.ai user id, attached only while you are logged in. + It is cleared on `logout` so telemetry stops identifying the previous user. + +The `device_id` is a random value with no link to your machine's hardware, +network, or account, so an event cannot be traced back to a specific person +through it. The `user_id` is your Kontent.ai user id and is only ever attached +while you are signed in. + +Both values are stored in the CLI config file with owner-only permissions +(`0600`): + +- Linux: `$XDG_CONFIG_HOME/kontent-cli/config.json` (defaults to + `~/.config/kontent-cli/config.json`) +- macOS: `~/.config/kontent-cli/config.json` +- Windows: `%APPDATA%\kontent-cli\config.json` + +## How to disable telemetry + +Telemetry is automatically off when: + +- the `DO_NOT_TRACK` environment variable is set; +- the `KONTENT_DO_NOT_TRACK` environment variable is set; +- it has been disabled in the config file (`telemetryEnabled: false`); +- a CI environment is detected; +- the build has no telemetry API key (e.g. local development builds). + +The resolution order is: `DO_NOT_TRACK` β†’ `KONTENT_DO_NOT_TRACK` β†’ config file β†’ +CI detection β†’ missing API key. Any one of these turns telemetry off. + +### CLI commands + +```sh +kontent telemetry enable # opt in +kontent telemetry disable # opt out +kontent telemetry status # show current state and the reason +``` + +The setting is stored in the config file. Note that the environment variables +above always take precedence: if `DO_NOT_TRACK` or `KONTENT_DO_NOT_TRACK` is +set, telemetry stays off even after `kontent telemetry enable`. + +### Environment variables + +| Variable | Effect | +| ------------------------- | ----------------------------------------------------------- | +| `DO_NOT_TRACK` | Disables telemetry (industry-standard opt-out). | +| `KONTENT_DO_NOT_TRACK` | Disables telemetry (Kontent.ai-specific opt-out). | +| `KONTENT_TELEMETRY_DEBUG` | Prints events to stderr without sending them (dry run). | + +## Retention + + + +## Third-party provider + +Telemetry is processed by [Amplitude](https://amplitude.com/) in the European +Union (EU) data center. + +## Privacy + +Telemetry is processed in the EU and handled in line with the GDPR. We collect +the minimum needed to improve the CLI, you can opt out at any time (see above), +and we never sell or monetize the data. + +For details on how Kontent.ai handles data, see the +[Privacy Policy](https://kontent.ai/privacy/). + +Questions or concerns: diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..a450df9 --- /dev/null +++ b/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.x/schema.json", + "extends": ["@kontent-ai/biome-config/base"], + "files": { + "includes": ["src/**/*.ts"] + }, + "linter": { + "rules": { + "correctness": { + "useImportExtensions": { + "level": "error", + "options": { "forceJsExtensions": true } + } + } + } + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9337cde --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import kontentAiConfig from "@kontent-ai/eslint-config"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores(["dist", "node_modules", "coverage"]), + { + extends: [kontentAiConfig], + files: ["src/**/*.ts"], + languageOptions: { + parserOptions: { + project: ["./tsconfig.json"], + }, + }, + }, +]); diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index da59433..0000000 --- a/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// For a detailed explanation regarding each configuration property, visit: -// https://jestjs.io/docs/en/configuration.html - -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - globals: { - 'ts-jest': { - babelConfig: true - } - }, - testRegex: 'tests/.*\.test\.ts$' -}; diff --git a/package.json b/package.json index c797dc3..18e49ad 100644 --- a/package.json +++ b/package.json @@ -1,73 +1,58 @@ { "name": "@kontent-ai/cli", - "version": "0.8.2", - "description": "Command line interface tool that can be used for generating and runningKontent.ai migration scripts", - "main": "./lib/index.js", - "types": "./lib/types/index.d.ts", - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "test": "jest", - "test:coverage": "jest --collect-coverage", - "lint": "eslint \"src/**/*.ts\" && prettier --check \"src/**/*.ts\"", - "lint:fix": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.ts\"", - "debug": "node --inspect ./lib/index.js migration run --all -e DEV", - "prepare": "husky install" - }, - "publishConfig": { - "access": "public" - }, - "lint-staged": { - "*.ts": [ - "npx eslint --fix", - "npx prettier --write" - ] - }, + "version": "0.9.0", + "description": "Kontent.ai command-line interface", + "type": "module", + "main": "./dist/index.mjs", "bin": { - "kontent": "./lib/index.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/kontent-ai/cli.git" + "kontent": "./dist/index.mjs" }, - "author": "Kontent s.r.o.", + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "clean": "rm -rf dist", + "start": "node dist/index.mjs", + "dev": "tsdown --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "biome:check": "biome check", + "biome:fix": "biome check --fix --unsafe", + "prepare": "pnpm run clean && pnpm run build" + }, + "keywords": [], + "author": "", "license": "MIT", - "bugs": { - "url": "https://github.com/kontent-ai/cli/issues" - }, - "prettier": { - "semi": true, - "singleQuote": true, - "tabWidth": 4, - "printWidth": 250 + "packageManager": "pnpm@11.8.0", + "devDependencies": { + "@biomejs/biome": "^2.4.12", + "@kontent-ai/biome-config": "^0.7.0", + "@kontent-ai/eslint-config": "^2.4.1", + "@types/node": "^22.13.10", + "@types/yargs": "^17.0.35", + "eslint": "^9.39.4", + "tsdown": "^0.21.10", + "typescript": "^5.9.3", + "vitest": "^4.1.9" }, - "homepage": "https://github.com/kontent-ai/cli#readme", "dependencies": { - "@kontent-ai/backup-manager": "4.2.4", - "chalk": "^4.1.2", - "dotenv": "^16.3.1", - "yargs": "^17.7.2" - }, - "peerDependencies": { - "@kontent-ai/management-sdk": "^5.0.0" - }, - "devDependencies": { - "@babel/core": "~7.18.10", - "@babel/preset-env": "~7.18.10", - "@babel/preset-typescript": "~7.18.6", - "@types/jest": "~28.1.7", - "@types/node": "~18.14.2", - "@types/yargs": "~17.0.11", - "@typescript-eslint/eslint-plugin": "^5.33.1", - "@typescript-eslint/parser": "^5.33.1", - "babel-jest": "~28.1.3", - "eslint": "^8.22.0", - "eslint-config-prettier": "^8.5.0", - "husky": "^8.0.1", - "jest": "~28.1.3", - "lint-staged": "^13.0.3", - "prettier": "^2.7.1", - "ts-jest": "^28.0.8", - "typescript": "~4.9.5" + "@amplitude/analytics-node": "^1.5.59", + "@clack/prompts": "^1.2.0", + "@kontent-ai/core-sdk": "12.0.0-preview.40", + "@kontent-ai/core-sdk-v10": "npm:@kontent-ai/core-sdk@10.12.5", + "@kontent-ai/management-sdk": "^8.5.1", + "@napi-rs/keyring": "^1.2.0", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", + "giget": "^3.2.0", + "open": "^10.2.0", + "openid-client": "^5.7.1", + "ts-pattern": "^5.9.0", + "yargs": "^18.0.0", + "zod": "^4.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..881a752 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4131 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@amplitude/analytics-node': + specifier: ^1.5.59 + version: 1.5.60 + '@clack/prompts': + specifier: ^1.2.0 + version: 1.6.0 + '@kontent-ai/core-sdk': + specifier: 12.0.0-preview.40 + version: 12.0.0-preview.40(ts-pattern@5.9.0)(zod@4.4.3) + '@kontent-ai/core-sdk-v10': + specifier: npm:@kontent-ai/core-sdk@10.12.5 + version: '@kontent-ai/core-sdk@10.12.5' + '@kontent-ai/management-sdk': + specifier: ^8.5.1 + version: 8.5.1 + '@napi-rs/keyring': + specifier: ^1.2.0 + version: 1.3.0 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + ci-info: + specifier: ^4.4.0 + version: 4.4.0 + giget: + specifier: ^3.2.0 + version: 3.3.0 + open: + specifier: ^10.2.0 + version: 10.2.0 + openid-client: + specifier: ^5.7.1 + version: 5.7.1 + ts-pattern: + specifier: ^5.9.0 + version: 5.9.0 + yargs: + specifier: ^18.0.0 + version: 18.0.0 + zod: + specifier: ^4.0.0 + version: 4.4.3 + devDependencies: + '@biomejs/biome': + specifier: ^2.4.12 + version: 2.5.0 + '@kontent-ai/biome-config': + specifier: ^0.7.0 + version: 0.7.3(@biomejs/biome@2.5.0) + '@kontent-ai/eslint-config': + specifier: ^2.4.1 + version: 2.4.1(typescript@5.9.3) + '@types/node': + specifier: ^22.13.10 + version: 22.20.0 + '@types/yargs': + specifier: ^17.0.35 + version: 17.0.35 + eslint: + specifier: ^9.39.4 + version: 9.39.4 + tsdown: + specifier: ^0.21.10 + version: 0.21.10(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@22.20.0)(vite@8.0.16(@types/node@22.20.0)) + +packages: + + '@amplitude/analytics-connector@1.6.4': + resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} + + '@amplitude/analytics-core@2.50.0': + resolution: {integrity: sha512-d8/U5aAHAs6XcpvQYpStxBxMxSwjDajZ0f6wMSwPCfxDGEWqcFaYuRKauh+vrn89D6gWcKi1VZ/tKCyhVby9sg==} + + '@amplitude/analytics-node@1.5.60': + resolution: {integrity: sha512-/1H4TR8kEvFYGWtu04dSoCywOf8yRE0dmil7Sy4iNPJDuQ9b6DWSDED2DOUuk8lBDDZ1AgQA24m3yssH0UrDkw==} + + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@8.0.2': + resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@8.0.0': + resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + '@babel/types@8.0.0': + resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@biomejs/biome@2.5.0': + resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.0': + resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.0': + resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.0': + resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.0': + resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.0': + resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.0': + resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@clack/core@1.4.2': + resolution: {integrity: sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.6.0': + resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} + engines: {node: '>= 20.12.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kontent-ai/biome-config@0.7.3': + resolution: {integrity: sha512-4Gi29ZkI4+F3e0+haJi4oBY7vy08ajb991LY+wNrBC5D+0Wc4RJheHRRrutrVUt5TfIEj1YG0p2Lc5V40gb1vg==} + peerDependencies: + '@biomejs/biome': ^2.5.0 + + '@kontent-ai/core-sdk@10.12.5': + resolution: {integrity: sha512-+EpX37RhdtORh7aGJuC1FboI8TNi8OUa/ksmJ3LByLsG+UyTcKEKAeTrp2B3rcZICIdYHTJ1Oi0C0v7wNxQB2g==} + engines: {node: '>= 20'} + + '@kontent-ai/core-sdk@12.0.0-preview.40': + resolution: {integrity: sha512-W8k5iijCvknnzHGX7q7VD0zzk2dh7W5elml2P76iroPj48OvxZuscR9Fzyg2W/Scefpe5Yy70ek4k/uw+RN77A==} + engines: {node: '>=22'} + peerDependencies: + ts-pattern: ^5 + zod: ^4 + + '@kontent-ai/eslint-config@2.4.1': + resolution: {integrity: sha512-4tIyNDDIxeYaPK17lMN00Ch1kqNbayLmrV+/tcia62XzCejR/cHcXcsWVGhmxS1RjyVpqhstnTWbUrcvmXge2Q==} + + '@kontent-ai/management-sdk@8.5.1': + resolution: {integrity: sha512-CD1+EnhnFxGS8IClI5/sg9U02TNGOcESnNQoYoO/P3qs0+CM5M5lblzs2s73d2WuHgwNrVkyNCJKuUGg8WpBqg==} + engines: {node: '>= 20'} + + '@napi-rs/keyring-darwin-arm64@1.3.0': + resolution: {integrity: sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/keyring-darwin-x64@1.3.0': + resolution: {integrity: sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/keyring-freebsd-x64@1.3.0': + resolution: {integrity: sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/keyring-linux-arm-gnueabihf@1.3.0': + resolution: {integrity: sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/keyring-linux-arm64-gnu@1.3.0': + resolution: {integrity: sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-arm64-musl@1.3.0': + resolution: {integrity: sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-linux-riscv64-gnu@1.3.0': + resolution: {integrity: sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-gnu@1.3.0': + resolution: {integrity: sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/keyring-linux-x64-musl@1.3.0': + resolution: {integrity: sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/keyring-win32-arm64-msvc@1.3.0': + resolution: {integrity: sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/keyring-win32-ia32-msvc@1.3.0': + resolution: {integrity: sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/keyring-win32-x64-msvc@1.3.0': + resolution: {integrity: sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/keyring@1.3.0': + resolution: {integrity: sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==} + engines: {node: '>= 10'} + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.20.0': + resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@types/zen-observable@0.8.3': + resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==} + + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@3.0.0: + resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + giget@3.3.0: + resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-without-cache@0.3.3: + resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} + engines: {node: '>=20.19.0'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + oidc-token-hash@5.2.0: + resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} + engines: {node: ^10.13.0 || >=12.0.0} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-json-stringify@1.2.0: + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + tsdown@0.21.10: + resolution: {integrity: sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.10 + '@tsdown/exe': 0.21.10 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrun@0.2.39: + resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zen-observable@0.10.0: + resolution: {integrity: sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@amplitude/analytics-connector@1.6.4': {} + + '@amplitude/analytics-core@2.50.0': + dependencies: + '@amplitude/analytics-connector': 1.6.4 + '@types/zen-observable': 0.8.3 + safe-json-stringify: 1.2.0 + tslib: 2.8.1 + zen-observable: 0.10.0 + + '@amplitude/analytics-node@1.5.60': + dependencies: + '@amplitude/analytics-core': 2.50.0 + tslib: 2.8.1 + + '@babel/generator@8.0.0-rc.3': + dependencies: + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@8.0.0': {} + + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + + '@babel/helper-validator-identifier@8.0.2': {} + + '@babel/parser@8.0.0': + dependencies: + '@babel/types': 8.0.0 + + '@babel/parser@8.0.0-rc.3': + dependencies: + '@babel/types': 8.0.0-rc.3 + + '@babel/types@8.0.0': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.2 + + '@babel/types@8.0.0-rc.3': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + + '@biomejs/biome@2.5.0': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.0 + '@biomejs/cli-darwin-x64': 2.5.0 + '@biomejs/cli-linux-arm64': 2.5.0 + '@biomejs/cli-linux-arm64-musl': 2.5.0 + '@biomejs/cli-linux-x64': 2.5.0 + '@biomejs/cli-linux-x64-musl': 2.5.0 + '@biomejs/cli-win32-arm64': 2.5.0 + '@biomejs/cli-win32-x64': 2.5.0 + + '@biomejs/cli-darwin-arm64@2.5.0': + optional: true + + '@biomejs/cli-darwin-x64@2.5.0': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.0': + optional: true + + '@biomejs/cli-linux-arm64@2.5.0': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.0': + optional: true + + '@biomejs/cli-linux-x64@2.5.0': + optional: true + + '@biomejs/cli-win32-arm64@2.5.0': + optional: true + + '@biomejs/cli-win32-x64@2.5.0': + optional: true + + '@clack/core@1.4.2': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.6.0': + dependencies: + '@clack/core': 1.4.2 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kontent-ai/biome-config@0.7.3(@biomejs/biome@2.5.0)': + dependencies: + '@biomejs/biome': 2.5.0 + + '@kontent-ai/core-sdk@10.12.5': + dependencies: + axios: 1.16.1 + transitivePeerDependencies: + - debug + - supports-color + + '@kontent-ai/core-sdk@12.0.0-preview.40(ts-pattern@5.9.0)(zod@4.4.3)': + dependencies: + ts-pattern: 5.9.0 + zod: 4.4.3 + + '@kontent-ai/eslint-config@2.4.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + eslint-plugin-react: 7.37.5(eslint@9.39.4) + transitivePeerDependencies: + - jiti + - supports-color + - typescript + + '@kontent-ai/management-sdk@8.5.1': + dependencies: + '@kontent-ai/core-sdk': 10.12.5 + mime: 3.0.0 + transitivePeerDependencies: + - debug + - supports-color + + '@napi-rs/keyring-darwin-arm64@1.3.0': + optional: true + + '@napi-rs/keyring-darwin-x64@1.3.0': + optional: true + + '@napi-rs/keyring-freebsd-x64@1.3.0': + optional: true + + '@napi-rs/keyring-linux-arm-gnueabihf@1.3.0': + optional: true + + '@napi-rs/keyring-linux-arm64-gnu@1.3.0': + optional: true + + '@napi-rs/keyring-linux-arm64-musl@1.3.0': + optional: true + + '@napi-rs/keyring-linux-riscv64-gnu@1.3.0': + optional: true + + '@napi-rs/keyring-linux-x64-gnu@1.3.0': + optional: true + + '@napi-rs/keyring-linux-x64-musl@1.3.0': + optional: true + + '@napi-rs/keyring-win32-arm64-msvc@1.3.0': + optional: true + + '@napi-rs/keyring-win32-ia32-msvc@1.3.0': + optional: true + + '@napi-rs/keyring-win32-x64-msvc@1.3.0': + optional: true + + '@napi-rs/keyring@1.3.0': + optionalDependencies: + '@napi-rs/keyring-darwin-arm64': 1.3.0 + '@napi-rs/keyring-darwin-x64': 1.3.0 + '@napi-rs/keyring-freebsd-x64': 1.3.0 + '@napi-rs/keyring-linux-arm-gnueabihf': 1.3.0 + '@napi-rs/keyring-linux-arm64-gnu': 1.3.0 + '@napi-rs/keyring-linux-arm64-musl': 1.3.0 + '@napi-rs/keyring-linux-riscv64-gnu': 1.3.0 + '@napi-rs/keyring-linux-x64-gnu': 1.3.0 + '@napi-rs/keyring-linux-x64-musl': 1.3.0 + '@napi-rs/keyring-win32-arm64-msvc': 1.3.0 + '@napi-rs/keyring-win32-ia32-msvc': 1.3.0 + '@napi-rs/keyring-win32-x64-msvc': 1.3.0 + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-project/types@0.133.0': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.17': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/jsesc@2.5.1': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.20.0': + dependencies: + undici-types: 6.21.0 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/zen-observable@0.8.3': {} + + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.61.1': {} + + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.1(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@22.20.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.16(@types/node@22.20.0) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.3.1: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-kit@3.0.0: + dependencies: + '@babel/parser': 8.0.0 + estree-walker: 3.0.3 + pathe: 2.0.3 + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.16.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + birpc@4.0.0: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@7.0.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + ci-info@4.4.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.7: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dts-resolver@2.1.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@10.6.0: {} + + empathic@2.0.1: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.1 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.1: + dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react@7.37.5(eslint@9.39.4): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.3 + eslint: 9.39.4 + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.16.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@3.3.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hookable@6.1.1: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-without-cache@0.3.3: {} + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@3.0.0: {} + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jose@4.15.9: {} + + js-tokens@4.0.0: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@3.0.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + natural-compare@1.4.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + object-assign@4.1.1: {} + + object-hash@2.2.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.1.3: {} + + oidc-token-hash@5.2.0: {} + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + quansync@1.0.0: {} + + react-is@16.13.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.17)(typescript@5.9.3): + dependencies: + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.14.0 + obug: 2.1.3 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + run-applescript@7.1.0: {} + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-json-stringify@1.2.0: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-pattern@5.9.0: {} + + tsdown@0.21.10(typescript@5.9.3): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.3.3 + obug: 2.1.3 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.17)(typescript@5.9.3) + semver: 7.8.5 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + unrun: 0.2.39 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@6.21.0: {} + + unrun@0.2.39: + dependencies: + rolldown: 1.0.0-rc.17 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.16(@types/node@22.20.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.0 + fsevents: 2.3.3 + + vitest@4.1.9(@types/node@22.20.0)(vite@8.0.16(@types/node@22.20.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@22.20.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@22.20.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.0 + transitivePeerDependencies: + - msw + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + zen-observable@0.10.0: {} + + zod@4.4.3: {} diff --git a/src/cmds/backup.ts b/src/cmds/backup.ts deleted file mode 100644 index 43950b3..0000000 --- a/src/cmds/backup.ts +++ /dev/null @@ -1,153 +0,0 @@ -import yargs from 'yargs'; -import chalk from 'chalk'; -import { environmentConfigExists, getEnvironmentsConfig } from '../utils/environmentUtils'; -import { CleanService, ExportService, ImportService, ZipService, IProcessedItem } from '@kontent-ai/backup-manager'; -import { getFileBackupName } from '../utils/fileUtils'; -import { FileService } from '@kontent-ai/backup-manager/dist/cjs/lib/node'; - -const kontentBackupCommand: yargs.CommandModule = { - command: 'backup', - describe: 'Kontent.ai backup tool to backup & restore environments through Management API.', - builder: (yargs: any) => - yargs - .options({ - action: { - alias: 'a', - describe: 'Action for backup', - type: 'string', - }, - name: { - alias: 'n', - describe: 'Name of zip file', - type: 'string', - }, - log: { - alias: 'l', - describe: 'Enables/Disables logging', - type: 'boolean', - default: true, - }, - 'environment-id': { - alias: 'eid', - describe: 'Environment ID to run the migration script on', - type: 'string', - }, - 'api-key': { - alias: 'k', - describe: 'Management API key', - type: 'string', - }, - environment: { - alias: 'e', - describe: 'Environment name', - type: 'string', - }, - 'preserve-workflow': { - alias: 'ep', - describe: 'Indicates if language variant workflow information should be preserved. Enabled by default', - type: 'boolean', - default: true, - }, - }) - .conflicts('environment', 'api-key') - .conflicts('environment', 'environment-id') - .check((args: any) => { - if (!args.environment && !(args.environmentId && args.apiKey)) { - throw new Error(chalk.red('Specify an environment or a environment ID with its Management API key.')); - } - - if (args.environment) { - if (!environmentConfigExists()) { - throw new Error(chalk.red(`Cannot find the environment configuration file. Add an environment named \"${args.environment}\" first.`)); - } - - const environments = getEnvironmentsConfig(); - - if (!environments[args.environment]) { - throw new Error(chalk.red(`Cannot find the \"${args.environment}\" environment.`)); - } - } - - return true; - }), - handler: async (argv: any) => { - let environmentId = argv.environmentId; - let apiKey = argv.apiKey; - if (argv.environment) { - const environments = getEnvironmentsConfig(); - - environmentId = environments[argv.environment].environmentId || argv.environmentId; - apiKey = environments[argv.environment].apiKey || argv.apiKey; - } - - const defaultBackupName = getFileBackupName(); - const zipService = new ZipService({ - context: 'node.js', - enableLog: argv.log, - }); - - console.log('Starting backup tool'); - - const fileService = new FileService({ - enableLog: argv.log, - }); - - switch (argv.action) { - case 'backup': - const exportService = new ExportService({ - apiKey: apiKey, - environmentId: environmentId, - onExport: (item: IProcessedItem) => { - if (argv.log) { - console.log(`Exported: ${item.title} | ${item.type}`); - } - }, - }); - const exportedData = await exportService.exportAllAsync(); - await zipService.createZipAsync(exportedData); - const backupZipData = await zipService.createZipAsync(exportedData); - await fileService.writeFileAsync(argv.name || defaultBackupName, backupZipData); - break; - - case 'restore': - const zipData = await zipService.extractZipAsync(await fileService.loadFileAsync(argv.name || defaultBackupName)); - const importService = new ImportService({ - onImport: (item: IProcessedItem) => { - if (argv.log) { - console.log(`Imported: ${item.title} | ${item.type}`); - } - }, - preserveWorkflow: argv.preserveWorkflow, - environmentId: environmentId, - apiKey: apiKey, - enableLog: argv.log, - fixLanguages: true, - }); - await importService.importFromSourceAsync(zipData); - break; - - case 'clean': - const cleanService = new CleanService({ - onDelete: (item: IProcessedItem) => { - if (argv.log) { - console.log(`Deleted: ${item.title} | ${item.type}`); - } - }, - environmentId: environmentId, - apiKey: apiKey, - }); - - await cleanService.cleanAllAsync(); - break; - - default: - throw new Error('Unknown action type'); - } - - console.log('Completed'); - process.exit(0); - }, -}; - -// yargs needs exported command in exports object -Object.assign(exports, kontentBackupCommand); diff --git a/src/cmds/environment.ts b/src/cmds/environment.ts deleted file mode 100644 index c55c0f5..0000000 --- a/src/cmds/environment.ts +++ /dev/null @@ -1,13 +0,0 @@ -import yargs from 'yargs'; - -const environmentCommand: yargs.CommandModule = { - command: 'environment ', - describe: 'Environment commands', - builder: (yargs: any) => { - return yargs.commandDir('environment').demandCommand(2, 'Please specify a environment arguments'); - }, - handler: (argv: any) => {}, -}; - -// yargs needs exported command in exports object -Object.assign(exports, environmentCommand); diff --git a/src/cmds/environment/add.ts b/src/cmds/environment/add.ts deleted file mode 100644 index c7d33b7..0000000 --- a/src/cmds/environment/add.ts +++ /dev/null @@ -1,33 +0,0 @@ -import yargs from 'yargs'; -import { saveEnvironmentConfig } from '../../utils/environmentUtils'; - -const addEnvironmentCommand: yargs.CommandModule = { - command: 'add', - describe: 'Store information about the environment locally. The environment is defined as a named pair of values. For example, a "DEV" environment can be defined as a pair of specific environment ID and Management API key.', - builder: (yargs: any) => - yargs - .options({ - name: { - alias: 'n', - describe: 'Environment name', - type: 'string', - }, - 'environment-id': { - alias: 'eid', - describe: 'Environment ID to run the migration script on', - type: 'string', - }, - 'api-key': { - alias: 'k', - describe: 'Management API key', - type: 'string', - }, - }) - .demandOption(['environment-id', 'api-key', 'name']), - handler: (argv: any) => { - saveEnvironmentConfig(argv.name, argv.environmentId, argv.apiKey); - }, -}; - -// yargs needs exported command in exports object -Object.assign(exports, addEnvironmentCommand); diff --git a/src/cmds/migration.ts b/src/cmds/migration.ts deleted file mode 100644 index 22e4db3..0000000 --- a/src/cmds/migration.ts +++ /dev/null @@ -1,13 +0,0 @@ -import yargs from 'yargs'; - -const migrationCommand: yargs.CommandModule = { - command: 'migration ', - describe: 'Migration commands', - builder: (yargs: any) => { - return yargs.commandDir('migration').demandCommand(2, 'Please specify a migration arguments'); - }, - handler: (argv: any) => {}, -}; - -// yargs needs exported command in exports object -Object.assign(exports, migrationCommand); diff --git a/src/cmds/migration/add.ts b/src/cmds/migration/add.ts deleted file mode 100644 index e35bac6..0000000 --- a/src/cmds/migration/add.ts +++ /dev/null @@ -1,43 +0,0 @@ -import yargs from 'yargs'; -import chalk from 'chalk'; -import { createMigration } from '../../utils/migrationUtils'; -import { TemplateType } from '../../models/templateType'; - -const addMigrationCommand: yargs.CommandModule = { - command: 'add', - describe: 'Generates a template script (in JavaScript or TypeScript) for running a migration on a Kontent.ai environment.', - builder: (yargs: any) => - yargs - .options({ - name: { - alias: 'n', - describe: 'Migration name', - type: 'string', - }, - 'template-type': { - alias: 't', - describe: 'Determines whether the template script is in TypeScript or plain JavaScript', - type: 'string', - default: 'javascript', - }, - 'timestamp-order': { - alias: 'd', - describe: 'Let order of the migrations be determined by the time it was created.', - type: 'boolean', - default: false, - }, - }) - .demandOption(['name', 'template-type', 'timestamp-order']), - handler: (argv: any) => { - if (!['javascript', 'typescript'].includes(argv.templateType)) { - console.error(chalk.redBright(`Unexpected template type ${argv.templateType} allowed is [typescript, javascript]`)); - process.exit(1); - } - - const templateType = argv.templateType === 'javascript' ? TemplateType.Javascript : TemplateType.TypeScript; - createMigration(argv.name, templateType, argv.timestampOrder); - }, -}; - -// yargs needs exported command in exports object -Object.assign(exports, addMigrationCommand); diff --git a/src/cmds/migration/run.ts b/src/cmds/migration/run.ts deleted file mode 100644 index d30770b..0000000 --- a/src/cmds/migration/run.ts +++ /dev/null @@ -1,339 +0,0 @@ -import yargs from 'yargs'; -import chalk from 'chalk'; -import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, loadMigrationFiles, loadModule, runMigration, getMigrationsWithInvalidOrder } from '../../utils/migrationUtils'; -import { fileExists, getFileWithExtension, isAllowedExtension } from '../../utils/fileUtils'; -import { environmentConfigExists, getEnvironmentsConfig } from '../../utils/environmentUtils'; -import { createManagementClient } from '../../managementClientFactory'; -import { getPluginsFilePath, loadMigrationsExecutionStatus } from '../../utils/statusManager'; -import { IMigration } from '../../models/migration'; -import { IRange } from '../../models/range'; -import { IStatus, Operation } from '../../models/status'; -import { loadStatusPlugin } from '../../utils/status/statusPlugin'; - -const runMigrationCommand: yargs.CommandModule = { - command: 'run', - describe: 'Runs a migration script specified by its name, or runs multiple migration scripts in the specified order.', - builder: (yargs: any) => - yargs - .options({ - name: { - alias: 'n', - describe: 'Migration name', - type: 'string', - }, - 'environment-id': { - alias: 'eid', - describe: 'Environment ID to run the migration script on', - type: 'string', - }, - 'api-key': { - alias: 'k', - describe: 'Management API key', - type: 'string', - }, - environment: { - alias: 'e', - describe: 'Environment name', - type: 'string', - }, - all: { - alias: 'a', - describe: 'Run all migration scripts in the specified order', - type: 'boolean', - }, - range: { - alias: 'r', - describe: 'Run all migration scripts in the specified range, eg.: 3:5 will run migrations with the "order" property set to 3, 4 and 5', - type: 'string', - }, - force: { - alias: 'f', - describe: 'Enforces run of already executed scripts.', - type: 'boolean', - default: false, - }, - 'continue-on-error': { - alias: 'c', - describe: 'Continue executing migration scripts even if a migration script fails.', - default: false, - type: 'boolean', - }, - 'log-http-service-errors-to-console': { - alias: 'l', - describe: 'Log HttpService errors to console log.', - default: false, - type: 'boolean', - }, - rollback: { - alias: 'b', - describe: 'Call rollback function from the migration', - default: false, - type: 'boolean', - }, - }) - .conflicts('all', 'name') - .conflicts('range', 'name') - .conflicts('all', 'range') - .conflicts('environment', 'api-key') - .conflicts('environment', 'environment-id') - .check((args: any) => { - if (!args.environment && !(args.environmentId && args.apiKey)) { - throw new Error(chalk.red('Specify an environment or a environment ID with its Management API key.')); - } - - if (!args.all) { - if (args.range) { - if (!getRange(args.range) && !getRangeDate(args.range)) { - throw new Error(chalk.red('The range has to be a string of a format "number:number" or "Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss" where the first value (to the left to ":") is less or equal to the second, eg.: "2:5".')); - } - } else if (args.name) { - if (!isAllowedExtension(args.name)) { - throw new Error(chalk.red(`File ${args.name} has not supported extension.`)); - } - const fileName = getFileWithExtension(args.name); - const migrationFilePath = getMigrationFilepath(fileName); - if (!fileExists(migrationFilePath)) { - throw new Error(chalk.red(`Cannot find the specified migration script: ${migrationFilePath}.`)); - } - } else { - throw new Error(chalk.red('Either the migration script name, range or all migration options needs to be specified.')); - } - } - - if (args.environment) { - if (!environmentConfigExists()) { - throw new Error(chalk.red(`Cannot find the environment configuration file. Add an environment named \"${args.environment}\" first.`)); - } - - const environments = getEnvironmentsConfig(); - - if (!environments[args.environment]) { - throw new Error(chalk.red(`Cannot find the \"${args.environment}\" environment.`)); - } - } - - return true; - }), - handler: async (argv: any): Promise => { - let environmentId = argv.environmentId; - let apiKey = argv.apiKey; - const migrationName = argv.name; - const runAll = argv.all; - const runRange = argv.range && (exports.getRange(argv.range) || getRangeDate(argv.range)); - const logHttpServiceErrorsToConsole = argv.logHttpServiceErrorsToConsole; - const continueOnError = argv.continueOnError; - const rollback = argv.rollback; - let migrationsResults: number = 0; - const runForce = argv.force; - - const operation: Operation = rollback ? 'rollback' : 'run'; - - if (argv.environment) { - const environments = getEnvironmentsConfig(); - - environmentId = environments[argv.environment].environmentId || argv.environmentId; - apiKey = environments[argv.environment].apiKey || argv.apiKey; - } - - const plugin = fileExists(getPluginsFilePath()) ? await loadStatusPlugin(getPluginsFilePath().slice(0, -3) + '.js') : undefined; - - const apiClient = createManagementClient({ - environmentId, - apiKey, - logHttpServiceErrorsToConsole, - }); - - const migrationOptions = { - client: apiClient, - environmentId: environmentId, - operation: operation, - saveStatusFromPlugin: plugin?.saveStatus ?? null, - }; - - let migrationsStatus: IStatus; - try { - migrationsStatus = await loadMigrationsExecutionStatus(plugin?.readStatus ?? null); - } catch (e) { - console.error(`An error ${chalk.red(e)} occured when trying to read status`); - process.exit(1); - } - - if (runAll || runRange) { - let migrationsToRun = await loadMigrationFiles(); - - checkForDuplicates(migrationsToRun); - checkForInvalidOrder(migrationsToRun); - - if (runRange) { - migrationsToRun = getMigrationsByRange(migrationsToRun, runRange); - } - - if (runForce) { - console.log('Skipping to check already executed migrations'); - } else { - migrationsToRun = skipExecutedMigrations(migrationsStatus, migrationsToRun, environmentId, operation); - } - - if (migrationsToRun.length === 0) { - console.log('No migrations to run.'); - } - - const sortedMigrationsToRun = migrationsToRun.sort(orderComparator(rollback)); - let executedMigrationsCount = 0; - for (const migration of sortedMigrationsToRun) { - const migrationResult = await runMigration(migrationsStatus, migration, migrationOptions); - - if (migrationResult > 0) { - if (!continueOnError) { - console.error(chalk.red(`Execution of the \"${migration.name}\" migration was not successful, stopping...`)); - console.error(chalk.red(`${executedMigrationsCount} of ${migrationsToRun.length} executed`)); - process.exit(1); - } - migrationsResults = 1; - } - - executedMigrationsCount++; - } - } else { - const fileName = getFileWithExtension(migrationName); - const migrationModule = await loadModule(fileName); - const migration = { - name: fileName, - module: migrationModule, - }; - - migrationsResults = await runMigration(migrationsStatus, migration, migrationOptions); - } - - process.exit(migrationsResults); - }, -}; - -export const getRange = (range: string): IRange | null => { - const match = range.match(/^([0-9]+):([0-9]+)$/); - if (!match) { - return null; - } - const from = Number(match[1]); - const to = Number(match[2]); - - return from <= to - ? { - from, - to, - } - : null; -}; - -export const getRangeDate = (range: string): IRange | null => { - // format is Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss - const match = range.match(/^T(?\d{4}((-\d{2}){0,2}))(?:-(?(\d{2}-){0,2}\d{2}))?:(?\d{4}((-\d{2}){0,2}))(?:-(?(\d{2}-){0,2}\d{2}))?$/); - if (!match) { - return null; - } - - const from = new Date(formatDate(match.groups?.from_date ?? '', match.groups?.from_time ?? '')); - const to = new Date(formatDate(match.groups?.to_date ?? '', match.groups?.to_time ?? '')); - - if (isNaN(from.getTime()) || isNaN(to.getTime())) { - return null; - } - - return from.getTime() <= to.getTime() - ? { - from, - to, - } - : null; -}; - -const checkForDuplicates = (migrationsToRun: IMigration[]): void => { - const duplicateMigrationsOrder = getDuplicates(migrationsToRun, (migration) => migration.module.order); - - if (duplicateMigrationsOrder.length > 0) { - console.log('Duplicate migrations found:'); - duplicateMigrationsOrder.map((migration) => console.error(chalk.red(`Migration: ${migration.name} order: ${migration.module.order}`))); - - process.exit(1); - } -}; - -const getMigrationsByRange = (migrationsToRun: IMigration[], range: IRange): IMigration[] => { - const migrations: IMigration[] = []; - - if (isIRangeDate(range)) { - for (const migration of migrationsToRun.filter((x) => x.module.order instanceof Date)) { - if ((migration.module.order as Date).getTime() >= range.from.getTime() && (migration.module.order as Date).getTime() <= range.to.getTime()) { - migrations.push(migration); - } - } - - return migrations.filter(String); - } - - for (const migration of migrationsToRun.filter((x) => typeof x.module.order === 'number')) { - if (migration.module.order >= range.from && migration.module.order <= range.to) { - migrations.push(migration); - } - } - - return migrations.filter(String); -}; - -const checkForInvalidOrder = (migrationsToRun: IMigration[]): void => { - const migrationsWithInvalidOrder: IMigration[] = getMigrationsWithInvalidOrder(migrationsToRun); - - if (migrationsWithInvalidOrder.length > 0) { - console.log('Migration order has to be positive integer or zero:'); - migrationsWithInvalidOrder.map((migration) => console.error(chalk.red(`Migration: ${migration.name} order: ${migration.module.order}`))); - - process.exit(1); - } -}; - -const skipExecutedMigrations = (migrationStatus: IStatus, migrations: IMigration[], environmentId: string, operation: Operation): IMigration[] => { - const executedMigrations = getSuccessfullyExecutedMigrations(migrationStatus, migrations, environmentId, operation); - const result: IMigration[] = []; - - for (const migration of migrations) { - if (executedMigrations.some((executedMigration) => executedMigration.name === migration.name)) { - console.log(`Skipping already executed migration ${migration.name}`); - } else { - result.push(migration); - } - } - - return result; -}; - -const comparator = (migrationPrev: IMigration, migrationNext: IMigration) => { - if (typeof migrationPrev.module.order === 'number' && typeof migrationNext.module.order === 'number') { - return migrationPrev.module.order - migrationNext.module.order; - } - - if (migrationPrev.module.order instanceof Date && migrationNext.module.order instanceof Date) { - return migrationPrev.module.order.getTime() - migrationNext.module.order.getTime(); - } - - return typeof migrationPrev.module.order === 'number' ? -1 : 1; -}; - -const orderComparator = (rollback: boolean) => (migrationPrev: IMigration, migrationNext: IMigration) => rollback ? -comparator(migrationPrev, migrationNext) : comparator(migrationPrev, migrationNext); - -const formatDate = (date: string, time: string) => { - if (time === '') { - time = '00:00'; - } - if (time.length === 2) { - time = time + ':00'; - } else { - time = time.replaceAll('-', ':'); - } - - return `${date}T${time}Z`; -}; - -const isIRangeDate = (x: IRange): x is IRange => x.from instanceof Date && x.to instanceof Date; - -// yargs needs exported command in exports object -Object.assign(exports, runMigrationCommand); diff --git a/src/commands/login/login.ts b/src/commands/login/login.ts new file mode 100644 index 0000000..e35a903 --- /dev/null +++ b/src/commands/login/login.ts @@ -0,0 +1,30 @@ +import { type LoginOutcome, performLogin } from "../../core/login/login.js"; +import { formatAuthError } from "../../lib/auth/formatAuthError.js"; +import { isErr } from "../../lib/result.js"; +import { logError, logInfo } from "../../log.js"; +import type { RegisterCommand } from "../../types/yargs.js"; + +export const register: RegisterCommand = (y, deps) => + y.command({ + command: "login", + describe: "Authenticate with Kontent.ai via Auth0 device flow", + builder: (b) => b, + handler: async (args) => { + const tracker = deps.telemetry.startCommandTracking("login", args); + + const result = await performLogin(args); + if (isErr(result)) { + tracker.fail(result.error.kind); + logError(args, formatAuthError(result.error)); + process.exitCode = 1; + return; + } + tracker.succeed(); + logInfo(args, "standard", formatLoginOutcome(result.value)); + }, + }); + +const formatLoginOutcome = (outcome: LoginOutcome): string => { + const base = outcome.isAlreadyAuthenticated ? "Already authenticated" : "Logged in"; + return outcome.identifier === null ? base : `${base} as ${outcome.identifier}`; +}; diff --git a/src/commands/logout/logout.ts b/src/commands/logout/logout.ts new file mode 100644 index 0000000..5ecd643 --- /dev/null +++ b/src/commands/logout/logout.ts @@ -0,0 +1,25 @@ +import { performLogout } from "../../core/logout/logout.js"; +import { formatAuthError } from "../../lib/auth/formatAuthError.js"; +import { isErr } from "../../lib/result.js"; +import { logError, logInfo } from "../../log.js"; +import type { RegisterCommand } from "../../types/yargs.js"; + +export const register: RegisterCommand = (y, deps) => + y.command({ + command: "logout", + describe: "Clear stored authentication tokens", + builder: (b) => b, + handler: async (args) => { + const tracker = deps.telemetry.startCommandTracking("logout", args); + + const result = await performLogout(args); + if (isErr(result)) { + tracker.fail(result.error.kind); + logError(args, formatAuthError(result.error)); + process.exitCode = 1; + return; + } + tracker.succeed(); + logInfo(args, "standard", "Logged out."); + }, + }); diff --git a/src/commands/project/project.ts b/src/commands/project/project.ts new file mode 100644 index 0000000..cc8886e --- /dev/null +++ b/src/commands/project/project.ts @@ -0,0 +1,20 @@ +import chalk from "chalk"; + +import type { RegisterCommand } from "../../types/yargs.js"; +import { register as registerSample } from "./sample/sample.js"; + +const subcommandsToRegister: ReadonlyArray = [registerSample]; + +export const register: RegisterCommand = (y, deps) => + y.command({ + command: "project", + describe: "Project-related commands", + builder: (sub) => + subcommandsToRegister + .reduce((current, registerSub) => registerSub(current, deps), sub) + .demandCommand(1, chalk.red("You need to provide a project subcommand.")) + .strict(), + handler: () => { + // parent command is a group; subcommands handle execution + }, + }); diff --git a/src/commands/project/sample/bootstrap.ts b/src/commands/project/sample/bootstrap.ts new file mode 100644 index 0000000..a6a371c --- /dev/null +++ b/src/commands/project/sample/bootstrap.ts @@ -0,0 +1,110 @@ +import { inspect } from "node:util"; +import { intro, note, outro } from "@clack/prompts"; +import type { KontentSdkError } from "@kontent-ai/core-sdk"; +import { match } from "ts-pattern"; +import { getAuthenticatedIapiClient } from "../../../core/iapi/authenticatedClient.js"; +import { + type BootstrapError, + type BootstrapParams, + performBootstrap, +} from "../../../core/project/bootstrap.js"; +import { supportedProjectTypes } from "../../../core/project/samples.js"; +import { formatAuthError } from "../../../lib/auth/formatAuthError.js"; +import { createMapiClient } from "../../../lib/mapi/client.js"; +import { isErr } from "../../../lib/result.js"; +import type { Telemetry } from "../../../lib/telemetry/tracking.js"; +import { type LogOptions, logError } from "../../../log.js"; +import type { RegisterCommand } from "../../../types/yargs.js"; + +export const register: RegisterCommand = (sub, deps) => + sub.command({ + command: "bootstrap", + describe: "Clone a sample app for an environment and wire its .env", + builder: (b) => + b + .option("envId", { + type: "string", + demandOption: true, + describe: "Environment ID (Guid)", + }) + .option("path", { + type: "string", + demandOption: true, + describe: "Target directory for the cloned app (must be empty or non-existent)", + }), + handler: async (args) => runBootstrap(args, deps.telemetry), + }); + +const runBootstrap = async (params: BootstrapParams, telemetry: Telemetry): Promise => { + const tracker = telemetry.startCommandTracking("project sample bootstrap", params); + intro("Bootstrap a Kontent.ai project"); + + const clientResult = await getAuthenticatedIapiClient(params); + if (isErr(clientResult)) { + tracker.fail(`auth:${clientResult.error.kind}`, { project: params.envId }); + logError(params, formatAuthError(clientResult.error)); + process.exitCode = 1; + return; + } + const iapiClient = clientResult.value; + const mapiClient = createMapiClient({ token: iapiClient.token, envId: params.envId }); + + const result = await performBootstrap(params, { iapiClient, mapiClient }); + if (isErr(result)) { + tracker.fail(bootstrapErrorCode(result.error), { project: params.envId }); + handleBootstrapError(params, result.error); + return; + } + + tracker.succeed({ + project: params.envId, + subscription: result.value.subscriptionId, + "sample-project-type": result.value.sampleProjectType, + }); + note(`cd ${params.path}\nnpm ci\nnpm run dev`, "Next steps"); + outro("Done."); +}; + +const bootstrapErrorCode = (error: BootstrapError): string => + match(error) + .with( + { kind: "project-info-failed" }, + (e) => `project-info-failed:${e.sdkError.details.reason}`, + ) + .with({ kind: "properties-failed" }, (e) => `properties-failed:${e.sdkError.details.reason}`) + .with({ kind: "list-keys-failed" }, (e) => `list-keys-failed:${e.sdkError.details.reason}`) + .with({ kind: "key-detail-failed" }, (e) => `key-detail-failed:${e.sdkError.details.reason}`) + .with({ kind: "create-key-failed" }, (e) => `create-key-failed:${e.sdkError.details.reason}`) + .otherwise((e) => e.kind); + +const handleBootstrapError = (params: LogOptions, error: BootstrapError): void => + match(error) + // soft exits: the user chose to stop or the environment is not eligible + .with({ kind: "aborted" }, (e) => { + outro(e.message); + }) + .with({ kind: "unsupported-sample" }, (e) => { + outro( + `Bootstrap is only supported for ${supportedProjectTypes} environments. SampleProjectType is "${e.sampleValue ?? "(not set)"}".`, + ); + }) + .otherwise((hardError) => { + logError(params, formatBootstrapError(hardError)); + process.exitCode = 1; + }); + +const formatBootstrapError = ( + error: Exclude, +): string => + match(error) + .with({ kind: "target-not-usable" }, (e) => e.message) + .with({ kind: "clone-failed" }, (e) => e.message) + .with({ kind: "project-info-failed" }, (e) => formatSdkError(e.sdkError)) + .with({ kind: "properties-failed" }, (e) => formatSdkError(e.sdkError)) + .with({ kind: "list-keys-failed" }, (e) => formatSdkError(e.sdkError)) + .with({ kind: "key-detail-failed" }, (e) => formatSdkError(e.sdkError)) + .with({ kind: "create-key-failed" }, (e) => formatSdkError(e.sdkError)) + .exhaustive(); + +const formatSdkError = (err: KontentSdkError): string => + `[${err.details.reason}] ${err.message}\nurl: ${err.url}\ndetails: ${inspect(err.details, { depth: 5, colors: false, breakLength: 100 })}`; diff --git a/src/commands/project/sample/sample.ts b/src/commands/project/sample/sample.ts new file mode 100644 index 0000000..8875712 --- /dev/null +++ b/src/commands/project/sample/sample.ts @@ -0,0 +1,20 @@ +import chalk from "chalk"; + +import type { RegisterCommand } from "../../../types/yargs.js"; +import { register as registerBootstrap } from "./bootstrap.js"; + +const subcommandsToRegister: ReadonlyArray = [registerBootstrap]; + +export const register: RegisterCommand = (y, deps) => + y.command({ + command: "sample", + describe: "Sample app commands", + builder: (sub) => + subcommandsToRegister + .reduce((current, registerSub) => registerSub(current, deps), sub) + .demandCommand(1, chalk.red("You need to provide a sample subcommand.")) + .strict(), + handler: () => { + // parent command is a group; subcommands handle execution + }, + }); diff --git a/src/commands/telemetry/disable.ts b/src/commands/telemetry/disable.ts new file mode 100644 index 0000000..9d82371 --- /dev/null +++ b/src/commands/telemetry/disable.ts @@ -0,0 +1,10 @@ +import { setTelemetryStatus } from "../../core/telemetry/settings.js"; +import type { RegisterCommand } from "../../types/yargs.js"; + +export const register: RegisterCommand = (sub) => + sub.command({ + command: "disable", + describe: "Disable anonymous usage telemetry", + builder: (b) => b, + handler: async (args) => setTelemetryStatus(args, false), + }); diff --git a/src/commands/telemetry/enable.ts b/src/commands/telemetry/enable.ts new file mode 100644 index 0000000..7b03463 --- /dev/null +++ b/src/commands/telemetry/enable.ts @@ -0,0 +1,10 @@ +import { setTelemetryStatus } from "../../core/telemetry/settings.js"; +import type { RegisterCommand } from "../../types/yargs.js"; + +export const register: RegisterCommand = (sub) => + sub.command({ + command: "enable", + describe: "Enable anonymous usage telemetry", + builder: (b) => b, + handler: async (args) => setTelemetryStatus(args, true), + }); diff --git a/src/commands/telemetry/status.ts b/src/commands/telemetry/status.ts new file mode 100644 index 0000000..de704ba --- /dev/null +++ b/src/commands/telemetry/status.ts @@ -0,0 +1,10 @@ +import { showTelemetryStatus } from "../../core/telemetry/settings.js"; +import type { RegisterCommand } from "../../types/yargs.js"; + +export const register: RegisterCommand = (sub) => + sub.command({ + command: "status", + describe: "Show whether telemetry is enabled and why", + builder: (b) => b, + handler: async (args) => showTelemetryStatus(args), + }); diff --git a/src/commands/telemetry/telemetry.ts b/src/commands/telemetry/telemetry.ts new file mode 100644 index 0000000..7dda9b8 --- /dev/null +++ b/src/commands/telemetry/telemetry.ts @@ -0,0 +1,26 @@ +import chalk from "chalk"; + +import type { RegisterCommand } from "../../types/yargs.js"; +import { register as registerDisable } from "./disable.js"; +import { register as registerEnable } from "./enable.js"; +import { register as registerStatus } from "./status.js"; + +const subcommandsToRegister: ReadonlyArray = [ + registerStatus, + registerEnable, + registerDisable, +]; + +export const register: RegisterCommand = (y, deps) => + y.command({ + command: "telemetry", + describe: "Inspect or change anonymous usage telemetry settings", + builder: (sub) => + subcommandsToRegister + .reduce((current, registerSub) => registerSub(current, deps), sub) + .demandCommand(1, chalk.red("You need to provide a telemetry subcommand.")) + .strict(), + handler: () => { + // parent command is a group; subcommands handle execution + }, + }); diff --git a/src/core/iapi/authenticatedClient.ts b/src/core/iapi/authenticatedClient.ts new file mode 100644 index 0000000..b95421c --- /dev/null +++ b/src/core/iapi/authenticatedClient.ts @@ -0,0 +1,18 @@ +import { getValidAccessToken } from "../../lib/auth/tokenAccess.js"; +import type { AuthError } from "../../lib/auth/types.js"; +import { createIapiClient, type IapiClient } from "../../lib/iapi/client.js"; +import { isErr, ok, type Result } from "../../lib/result.js"; +import type { LogOptions } from "../../log.js"; +import { ensureUserIdCached } from "../user/user.js"; + +export const getAuthenticatedIapiClient = async ( + params: LogOptions, +): Promise> => { + const tokenResult = await getValidAccessToken(); + if (isErr(tokenResult)) { + return tokenResult; + } + const client = createIapiClient({ token: tokenResult.value }); + await ensureUserIdCached(params, { client }); + return ok(client); +}; diff --git a/src/core/login/login.ts b/src/core/login/login.ts new file mode 100644 index 0000000..6a78883 --- /dev/null +++ b/src/core/login/login.ts @@ -0,0 +1,135 @@ +import open from "open"; +import { match } from "ts-pattern"; +import { getAuth0Config } from "../../lib/auth/config.js"; +import { type DeviceFlowDeps, loginViaDeviceFlow } from "../../lib/auth/deviceFlow.js"; +import { formatAuthError } from "../../lib/auth/formatAuthError.js"; +import { decodeIdTokenClaims } from "../../lib/auth/idTokenClaims.js"; +import { logVerboseAuthInfo } from "../../lib/auth/logVerboseAuthInfo.js"; +import { createKeyringStorage, type TokenStorage } from "../../lib/auth/storage.js"; +import { decideAuth, refreshOrClear } from "../../lib/auth/tokenAccess.js"; +import type { AuthError, TokenSet } from "../../lib/auth/types.js"; +import { createIapiClient } from "../../lib/iapi/client.js"; +import { err, isErr, isOk, ok, type Result } from "../../lib/result.js"; +import { type LogOptions, logInfo, logWarning } from "../../log.js"; +import { ensureUserIdCached } from "../user/user.js"; + +export type LoginParams = LogOptions; + +export type LoginOutcome = Readonly<{ + isAlreadyAuthenticated: boolean; + identifier: string | null; +}>; + +export const performLogin = async ( + params: LoginParams, +): Promise> => { + const config = getAuth0Config(); + const storage = createKeyringStorage(); + + const stored = await storage.read(); + if (isErr(stored)) { + logWarning(params, "verbose", formatAuthError(stored.error)); + } + const storedTokens = isOk(stored) ? stored.value : null; + + const decision = decideAuth(storedTokens, Date.now()); + + return match(decision) + .with({ type: "use-existing-token" }, async () => { + if (storedTokens !== null) { + await ensureUserIdCached(params, { + client: createIapiClient({ token: storedTokens.accessToken }), + }); + } + return ok({ isAlreadyAuthenticated: true, identifier: identifierFromTokens(storedTokens) }); + }) + .with({ type: "refresh-token" }, async ({ refreshToken }) => { + const refreshed = await refreshOrClear(storage, config, refreshToken); + if (isErr(refreshed)) { + return err(refreshed.error); + } + await persistTokens(params, storage, refreshed.value); + await ensureUserIdCached(params, { + client: createIapiClient({ token: refreshed.value.accessToken }), + }); + await logVerboseAuthInfo(params, config, refreshed.value); + return ok({ + isAlreadyAuthenticated: false, + identifier: identifierFromTokens(refreshed.value), + }); + }) + .with({ type: "login" }, async () => { + const result = await loginViaDeviceFlow(config, deviceFlowDeps(params)); + if (isErr(result)) { + return err(result.error); + } + await persistTokens(params, storage, result.value); + // Fresh login may be a different account, so overwrite the cached userId. + await ensureUserIdCached(params, { + client: createIapiClient({ token: result.value.accessToken }), + shouldForceRefresh: true, + }); + await logVerboseAuthInfo(params, config, result.value); + return ok({ + isAlreadyAuthenticated: false, + identifier: identifierFromTokens(result.value), + }); + }) + .exhaustive(); +}; + +const persistTokens = async ( + params: LogOptions, + storage: TokenStorage, + tokens: TokenSet, +): Promise => { + const written = await storage.write(tokens); + if (isErr(written)) { + logWarning(params, "standard", formatAuthError(written.error)); + } +}; + +const identifierFromTokens = (tokens: TokenSet | null): string | null => { + if (tokens === null || tokens.idToken === undefined) { + return null; + } + return pickIdentifier(decodeIdTokenClaims(tokens.idToken)); +}; + +const pickIdentifier = (claims: Record): string | null => { + const candidates = [claims.email, claims.preferred_username, claims.sub]; + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.length > 0) { + return candidate; + } + } + return null; +}; + +const deviceFlowDeps = (params: LogOptions): DeviceFlowDeps => ({ + onUserCode: async ({ userCode, expiresInSeconds, verificationUriComplete }) => { + logInfo( + params, + "standard", + `Press Enter to open the browser. Code: ${userCode} (expires in ${formatExpiry(expiresInSeconds)}).`, + ); + await waitForEnter(); + await open(verificationUriComplete); + }, + nowMs: () => Date.now(), +}); + +const formatExpiry = (seconds: number): string => + seconds % 60 === 0 ? `${seconds / 60} minutes` : `${seconds} seconds`; + +const waitForEnter = async (): Promise => { + await new Promise((resolve) => { + const onData = () => { + process.stdin.removeListener("data", onData); + process.stdin.pause(); + resolve(); + }; + process.stdin.resume(); + process.stdin.once("data", onData); + }); +}; diff --git a/src/core/logout/logout.ts b/src/core/logout/logout.ts new file mode 100644 index 0000000..1410601 --- /dev/null +++ b/src/core/logout/logout.ts @@ -0,0 +1,21 @@ +import { createKeyringStorage } from "../../lib/auth/storage.js"; +import type { AuthError } from "../../lib/auth/types.js"; +import { writeCliConfig } from "../../lib/config/cliConfig.js"; +import { err, isErr, ok, type Result } from "../../lib/result.js"; +import { type LogOptions, logWarning } from "../../log.js"; + +export type LogoutParams = LogOptions; + +export const performLogout = async (params: LogoutParams): Promise> => { + const storage = createKeyringStorage(); + const cleared = await storage.clear(); + if (isErr(cleared)) { + return err(cleared.error); + } + // Drop the cached userId so telemetry stops identifying the previous user. + const clearedUserId = await writeCliConfig({ userId: undefined }); + if (isErr(clearedUserId)) { + logWarning(params, "verbose", `Could not clear cached userId: ${clearedUserId.error}`); + } + return ok(undefined); +}; diff --git a/src/core/project/bootstrap.ts b/src/core/project/bootstrap.ts new file mode 100644 index 0000000..9576c38 --- /dev/null +++ b/src/core/project/bootstrap.ts @@ -0,0 +1,289 @@ +import { readdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { confirm, isCancel, note, select, spinner } from "@clack/prompts"; +import type { KontentSdkError } from "@kontent-ai/core-sdk"; +import { downloadTemplate } from "giget"; +import { applyEnvOverrides } from "../../lib/envFile.js"; +import { errorMessage } from "../../lib/error.js"; +import type { IapiClient } from "../../lib/iapi/client.js"; +import { createDeliveryApiKey } from "../../lib/iapi/endpoints/createDeliveryApiKey.js"; +import { getApiKeyDetail } from "../../lib/iapi/endpoints/getApiKeyDetail.js"; +import { getProjectInfo } from "../../lib/iapi/endpoints/getProjectInfo.js"; +import { type ApiKeyListingItem, listApiKeys } from "../../lib/iapi/endpoints/listApiKeys.js"; +import { listProjectProperties } from "../../lib/iapi/endpoints/listProjectProperties.js"; +import type { MapiClient } from "../../lib/mapi/client.js"; +import { err, isErr, ok, type Result } from "../../lib/result.js"; +import { type LogOptions, logError, logWarning } from "../../log.js"; +import { buildEnvValues, findSample, type SampleApp } from "./samples.js"; +import { ensureLocalhostSpace, PREVIEW_PORT, type SpaceSetupError } from "./space.js"; + +export type BootstrapParams = LogOptions & + Readonly<{ + envId: string; + path: string; + }>; + +export type BootstrapClients = Readonly<{ + iapiClient: IapiClient; + mapiClient: MapiClient; +}>; + +export type BootstrapSuccess = Readonly<{ + subscriptionId: string; + sampleProjectType: string | undefined; +}>; + +export type BootstrapError = + | { readonly kind: "target-not-usable"; readonly message: string } + | { readonly kind: "project-info-failed"; readonly sdkError: KontentSdkError } + | { readonly kind: "properties-failed"; readonly sdkError: KontentSdkError } + | { readonly kind: "unsupported-sample"; readonly sampleValue: string | undefined } + | { readonly kind: "aborted"; readonly message: string } + | { readonly kind: "list-keys-failed"; readonly sdkError: KontentSdkError } + | { readonly kind: "key-detail-failed"; readonly sdkError: KontentSdkError } + | { readonly kind: "create-key-failed"; readonly sdkError: KontentSdkError } + | { readonly kind: "clone-failed"; readonly message: string }; + +const ENV_OUTPUT_FILE = ".env.local"; +const CREATE_NEW_KEY_VALUE = "__create_new_delivery_key__"; + +export const performBootstrap = async ( + params: BootstrapParams, + clients: BootstrapClients, +): Promise> => { + const { iapiClient, mapiClient } = clients; + + const targetCheck = await ensureTargetUsable(params.path); + if (targetCheck.kind === "err") { + return err({ kind: "target-not-usable", message: targetCheck.error }); + } + + const inspectSpinner = spinner(); + inspectSpinner.start("Inspecting environment"); + const projectResult = await getProjectInfo(iapiClient, params.envId).fetchSafe(); + if (!projectResult.success) { + inspectSpinner.error("Failed to fetch project info"); + return err({ kind: "project-info-failed", sdkError: projectResult.error }); + } + const { projectContainerId, projectName, subscriptionId } = projectResult.response.payload; + + const propertiesResult = await listProjectProperties(iapiClient, params.envId).fetchSafe(); + if (!propertiesResult.success) { + inspectSpinner.error("Failed to fetch project properties"); + return err({ kind: "properties-failed", sdkError: propertiesResult.error }); + } + inspectSpinner.stop(`Environment "${projectName}"`); + + const sampleProperty = propertiesResult.response.payload.find( + (p) => p.key === "SampleProjectType", + ); + const sampleValue = sampleProperty?.value; + const sample = findSample(sampleValue); + if (sample === undefined) { + return err({ kind: "unsupported-sample", sampleValue }); + } + + const keyResolution = await resolveDeliveryKey(iapiClient, projectContainerId, params.envId); + if (isErr(keyResolution)) { + return keyResolution; + } + const deliveryKey = keyResolution.value; + + const cloneSpinner = spinner(); + cloneSpinner.start(`Cloning ${sample.templateRepo}`); + try { + await downloadTemplate(sample.templateRepo, { dir: params.path, force: false }); + } catch (cause) { + cloneSpinner.error("Clone failed"); + return err({ kind: "clone-failed", message: errorMessage(cause) }); + } + cloneSpinner.stop(`Cloned into ${params.path}`); + + await wireEnvFile(params, sample, deliveryKey); + + if (sample.hasPreviewSpace) { + await setupLocalhostSpace(params, mapiClient); + } + + return ok({ subscriptionId, sampleProjectType: sampleValue }); +}; + +const setupLocalhostSpace = async ( + params: BootstrapParams, + mapiClient: MapiClient, +): Promise => { + const spaceSpinner = spinner(); + spaceSpinner.start("Setting up localhost preview space"); + const result = await ensureLocalhostSpace(mapiClient); + + if (isErr(result)) { + spaceSpinner.error("Could not set up the localhost preview space"); + logWarning(params, "standard", spaceWarning(result.error)); + return; + } + + spaceSpinner.stop('Space "localhost" ready'); + note( + result.value.wasSet + ? `Preview URL set to ${result.value.previewDomain}. Your app is expected on port ${PREVIEW_PORT}; if you run it on a different port, change the preview URL in the Kontent.ai app (Environment settings β†’ Preview URLs).` + : `The "localhost" space already has a preview URL (${result.value.previewDomain}); leaving it as is.`, + "Preview space ready", + ); +}; + +const spaceWarning = (error: SpaceSetupError): string => + error.kind === "preview-failed" + ? `The "localhost" space is ready, but its preview URL could not be set to localhost:${PREVIEW_PORT}.\n${error.message}\nSet it manually in the Kontent.ai app (Environment settings β†’ Preview URLs). Re-running bootstrap will retry.` + : `Could not create the "localhost" preview space.\n${error.message}\nAdd it manually in the Kontent.ai app (Environment settings β†’ Spaces).`; + +const resolveDeliveryKey = async ( + client: IapiClient, + containerId: string, + envId: string, +): Promise> => { + const listSpinner = spinner(); + listSpinner.start("Loading delivery API keys"); + const listResult = await listApiKeys(client, containerId, { + apiKeyTypes: ["delivery-api"], + environments: [envId], + }).executeSafe(); + + if (!listResult.success) { + listSpinner.error("Failed to list API keys"); + return err({ kind: "list-keys-failed", sdkError: listResult.error }); + } + + const existing = listResult.response.payload; + listSpinner.stop( + existing.length === 0 + ? "No delivery API key found" + : `Found ${existing.length} delivery API key${existing.length === 1 ? "" : "s"}`, + ); + + if (existing.length === 0) { + const wantsNew = await confirm({ + message: "Create a new delivery API key for this environment?", + initialValue: true, + }); + if (isCancel(wantsNew) || !wantsNew) { + return err({ + kind: "aborted", + message: "Aborted: a delivery API key is required to bootstrap.", + }); + } + return createKeyAndReturnSecret(client, containerId, envId); + } + + const choice = await select({ + message: "Select a delivery API key for the new app:", + options: [ + ...existing.map((k) => ({ value: k.token_seed_id, label: formatKeyOption(k) })), + { value: CREATE_NEW_KEY_VALUE, label: "+ Create new delivery key" }, + ], + }); + if (isCancel(choice)) { + return err({ kind: "aborted", message: "Aborted: no delivery API key selected." }); + } + + if (choice === CREATE_NEW_KEY_VALUE) { + return createKeyAndReturnSecret(client, containerId, envId); + } + + const detailSpinner = spinner(); + detailSpinner.start("Fetching API key"); + const detail = await getApiKeyDetail(client, containerId, choice).fetchSafe(); + if (!detail.success) { + detailSpinner.error("Failed to fetch API key"); + return err({ kind: "key-detail-failed", sdkError: detail.error }); + } + detailSpinner.stop("API key ready"); + return ok(detail.response.payload.api_key); +}; + +const createKeyAndReturnSecret = async ( + client: IapiClient, + containerId: string, + envId: string, +): Promise> => { + const expiresAt = new Date(); + expiresAt.setUTCFullYear(expiresAt.getUTCFullYear() + 1); + const createSpinner = spinner(); + createSpinner.start("Creating delivery API key"); + const created = await createDeliveryApiKey(client, containerId, { + name: `kontent-cli bootstrap (${envId.slice(0, 8)})`, + environments: [envId], + hasPreviewDeliveryAccess: true, + expiresAt, + }).executeSafe(); + if (!created.success) { + createSpinner.error("Failed to create delivery API key"); + return err({ kind: "create-key-failed", sdkError: created.error }); + } + createSpinner.stop(`Created delivery API key "${created.response.payload.name}"`); + return ok(created.response.payload.api_key); +}; + +const formatKeyOption = (k: ApiKeyListingItem): string => + `${k.name} (${k.token_seed_id.slice(0, 8)}…, expires ${k.expires_at.slice(0, 10)})`; + +const ensureTargetUsable = async ( + target: string, +): Promise<{ kind: "ok" } | { kind: "err"; error: string }> => { + try { + const entries = await readdir(target); + if (entries.length === 0) { + return { kind: "ok" }; + } + return { + kind: "err", + error: `Target path "${target}" is not empty. Choose a fresh directory.`, + }; + } catch (cause) { + if (isNoEnt(cause)) { + return { kind: "ok" }; + } + return { + kind: "err", + error: `Cannot access target path "${target}": ${errorMessage(cause)}`, + }; + } +}; + +const wireEnvFile = async ( + params: BootstrapParams, + sample: SampleApp, + deliveryKey: string, +): Promise => { + const templatePath = path.join(params.path, sample.envTemplateFile); + const envPath = path.join(params.path, ENV_OUTPUT_FILE); + const envSpinner = spinner(); + envSpinner.start(`Writing ${ENV_OUTPUT_FILE}`); + + let template: string; + try { + template = await readFile(templatePath, "utf8"); + } catch (cause) { + if (isNoEnt(cause)) { + envSpinner.stop( + `${sample.envTemplateFile} not found in template; skipping ${ENV_OUTPUT_FILE} wiring`, + ); + return; + } + envSpinner.error(`Failed to read ${sample.envTemplateFile}`); + logError(params, errorMessage(cause)); + return; + } + + const next = applyEnvOverrides(template, buildEnvValues(sample, params.envId, deliveryKey)); + + try { + await writeFile(envPath, next, "utf8"); + envSpinner.stop(`Wrote ${ENV_OUTPUT_FILE}`); + } catch (cause) { + envSpinner.error(`Failed to write ${ENV_OUTPUT_FILE}`); + logError(params, errorMessage(cause)); + } +}; + +const isNoEnt = (cause: unknown): boolean => + typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ENOENT"; diff --git a/src/core/project/samples.ts b/src/core/project/samples.ts new file mode 100644 index 0000000..5d74254 --- /dev/null +++ b/src/core/project/samples.ts @@ -0,0 +1,63 @@ +import { match } from "ts-pattern"; + +type BaseSample = Readonly<{ + templateRepo: string; // giget spec + envTemplateFile: string; + envIdVarName: string; + apiKeyVarName: string; + hasPreviewSpace?: boolean; +}>; + +export type SampleApp = + | (BaseSample & { readonly projectType: "Kickstart" }) + | (BaseSample & { readonly projectType: "Karma" }) + | (BaseSample & { + readonly projectType: "Ficto"; + readonly collectionVarName: string; + readonly collection: string; + }); + +const SAMPLES: readonly SampleApp[] = [ + { + projectType: "Kickstart", + templateRepo: "github:kontent-ai/kickstart-react-app", + envTemplateFile: ".env.template", + envIdVarName: "VITE_ENVIRONMENT_ID", + apiKeyVarName: "VITE_DELIVERY_API_KEY", + }, + { + projectType: "Karma", + templateRepo: "github:kontent-ai/karma-nextjs-app", + envTemplateFile: ".env.template", + envIdVarName: "KONTENT_ENVIRONMENT_ID", + apiKeyVarName: "KONTENT_DELIVERY_API_KEY", + hasPreviewSpace: true, + }, + { + projectType: "Ficto", + templateRepo: "github:kontent-ai/sample-app-next-js", + envTemplateFile: ".env.local.template", + envIdVarName: "NEXT_PUBLIC_KONTENT_ENVIRONMENT_ID", + apiKeyVarName: "KONTENT_PREVIEW_API_KEY", + collectionVarName: "NEXT_PUBLIC_KONTENT_COLLECTION_CODENAME", + collection: "ficto_healthtech", + }, +]; + +export const findSample = (projectType: string | undefined): SampleApp | undefined => + SAMPLES.find((sample) => sample.projectType === projectType); + +export const supportedProjectTypes = SAMPLES.map((sample) => sample.projectType).join(", "); + +export const buildEnvValues = ( + sample: SampleApp, + envId: string, + apiKey: string, +): Record => + match(sample) + .with({ projectType: "Ficto" }, (s) => ({ + [s.envIdVarName]: envId, + [s.apiKeyVarName]: apiKey, + [s.collectionVarName]: s.collection, + })) + .otherwise((s) => ({ [s.envIdVarName]: envId, [s.apiKeyVarName]: apiKey })); diff --git a/src/core/project/space.ts b/src/core/project/space.ts new file mode 100644 index 0000000..fd5e717 --- /dev/null +++ b/src/core/project/space.ts @@ -0,0 +1,91 @@ +import { mapiErrorMessage } from "../../lib/error.js"; +import type { MapiClient } from "../../lib/mapi/client.js"; +import { isErr, ok, type Result, tryAsync } from "../../lib/result.js"; + +const SPACE_NAME = "localhost"; +const SPACE_CODENAME = "localhost"; +export const PREVIEW_PORT = 3000; +const PREVIEW_DOMAIN = `localhost:${PREVIEW_PORT}`; + +export type SpaceSetupError = Readonly<{ + kind: "space-failed" | "preview-failed"; + message: string; +}>; + +export type SpaceSetupResult = Readonly<{ + previewDomain: string; + wasSet: boolean; +}>; + +export const ensureLocalhostSpace = async ( + mapiClient: MapiClient, +): Promise> => { + const existing = await findSpaceId(mapiClient, SPACE_CODENAME); + if (isErr(existing)) { + return existing; + } + + const spaceResult = + existing.value !== undefined + ? ok(existing.value) + : await createSpace(mapiClient, SPACE_NAME, SPACE_CODENAME); + if (isErr(spaceResult)) { + return spaceResult; + } + + return ensurePreviewDomain(mapiClient, spaceResult.value, PREVIEW_DOMAIN); +}; + +const findSpaceId = async ( + client: MapiClient, + codename: string, +): Promise> => + tryAsync( + async () => { + const spaces = (await client.listSpaces().toPromise()).data; + return spaces.find((space) => space.codename === codename)?.id; + }, + (cause) => ({ kind: "space-failed", message: mapiErrorMessage(cause) }), + ); + +const createSpace = async ( + client: MapiClient, + name: string, + codename: string, +): Promise> => + tryAsync( + async () => (await client.addSpace().withData({ name, codename }).toPromise()).data.id, + (cause) => ({ kind: "space-failed", message: mapiErrorMessage(cause) }), + ); + +const ensurePreviewDomain = async ( + client: MapiClient, + spaceId: string, + domain: string, +): Promise> => + tryAsync( + async () => { + const current = (await client.getPreviewConfiguration().toPromise()).data._raw; + const existing = current.space_domains.find((entry) => entry.space.id === spaceId); + if (existing !== undefined && existing.domain !== "") { + return { previewDomain: existing.domain, wasSet: false }; + } + + const ourEntry = { space: { id: spaceId }, domain }; + const spaceDomains = + existing !== undefined + ? current.space_domains.map((entry) => (entry.space.id === spaceId ? ourEntry : entry)) + : [...current.space_domains, ourEntry]; + + await client + .modifyPreviewConfiguration() + .withData({ + space_domains: spaceDomains, + preview_url_patterns: current.preview_url_patterns, + }) + .toPromise(); + + return { previewDomain: domain, wasSet: true }; + }, + (cause) => ({ kind: "preview-failed", message: mapiErrorMessage(cause) }), + ); diff --git a/src/core/telemetry/settings.ts b/src/core/telemetry/settings.ts new file mode 100644 index 0000000..7f46bdc --- /dev/null +++ b/src/core/telemetry/settings.ts @@ -0,0 +1,66 @@ +import { isCI } from "ci-info"; +import { getCliConfigPath, readCliConfig, writeCliConfig } from "../../lib/config/cliConfig.js"; +import { isTruthyEnv } from "../../lib/env.js"; +import { isErr } from "../../lib/result.js"; +import { formatTelemetryOffReason, resolveTelemetryConsent } from "../../lib/telemetry/consent.js"; +import { amplitudeApiKey } from "../../lib/telemetry/context.js"; +import { type LogOptions, logError, logInfo, logWarning } from "../../log.js"; + +export type TelemetryCommandParams = LogOptions; + +export const showTelemetryStatus = async (params: TelemetryCommandParams): Promise => { + const config = await readCliConfig(); + const consent = resolveTelemetryConsent(process.env, config, amplitudeApiKey, isCI); + + const reasonLine = consent.isEnabled + ? config.telemetryEnabled === true + ? "Reason: enabled in the config file" + : "Reason: default (no opt-out detected)" + : `Reason: ${formatTelemetryOffReason(consent.reason)}`; + + logInfo( + params, + "standard", + [ + `Telemetry: ${consent.isEnabled ? "enabled" : "disabled"}`, + reasonLine, + `Config file: ${getCliConfigPath()}`, + ].join("\n"), + ); +}; + +export const setTelemetryStatus = async ( + params: TelemetryCommandParams, + isEnabled: boolean, +): Promise => { + const written = await writeCliConfig({ + telemetryEnabled: isEnabled, + telemetryNoticeShown: true, + }); + if (isErr(written)) { + logError(params, `Failed to update telemetry config: ${written.error}`); + process.exitCode = 1; + return; + } + logInfo(params, "standard", isEnabled ? "Telemetry enabled." : "Telemetry disabled."); + if (isEnabled) { + warnIfEnvForcesOff(params); + } +}; + +const warnIfEnvForcesOff = (params: TelemetryCommandParams): void => { + if (isTruthyEnv(process.env.DO_NOT_TRACK)) { + logWarning( + params, + "standard", + "Note: DO_NOT_TRACK is set, so telemetry stays off in this environment.", + ); + } + if (isTruthyEnv(process.env.KONTENT_DO_NOT_TRACK)) { + logWarning( + params, + "standard", + "Note: KONTENT_DO_NOT_TRACK is set, so telemetry stays off in this environment.", + ); + } +}; diff --git a/src/core/user/user.ts b/src/core/user/user.ts new file mode 100644 index 0000000..e053b78 --- /dev/null +++ b/src/core/user/user.ts @@ -0,0 +1,47 @@ +import type { KontentSdkError } from "@kontent-ai/core-sdk"; + +import type { AuthError } from "../../lib/auth/types.js"; +import { readCliConfig, writeCliConfig } from "../../lib/config/cliConfig.js"; +import type { IapiClient } from "../../lib/iapi/client.js"; +import { getUser, type UserInfo } from "../../lib/iapi/endpoints/getUser.js"; +import { err, isErr, ok, type Result } from "../../lib/result.js"; +import { type LogOptions, logWarning } from "../../log.js"; + +export type UserError = + | { readonly kind: "auth-failed"; readonly authError: AuthError } + | { readonly kind: "fetch-failed"; readonly sdkError: KontentSdkError }; + +type EnsureUserIdOptions = Readonly<{ client: IapiClient; shouldForceRefresh?: boolean }>; + +// Best-effort: never throws, so a /user failure can't break login. +export const ensureUserIdCached = async ( + params: LogOptions, + options: EnsureUserIdOptions, +): Promise => { + const cached = (await readCliConfig()).userId; + if (cached !== undefined && options.shouldForceRefresh !== true) { + return; + } + + const result = await fetchUser(options.client); + if (isErr(result)) { + logWarning(params, "verbose", `Could not cache userId: ${formatUserError(result.error)}`); + return; + } + + const written = await writeCliConfig({ userId: result.value.userId }); + if (isErr(written)) { + logWarning(params, "verbose", `Could not persist userId: ${written.error}`); + } +}; + +const fetchUser = async (client: IapiClient): Promise> => { + const result = await getUser(client).fetchSafe(); + if (!result.success) { + return err({ kind: "fetch-failed", sdkError: result.error }); + } + return ok(result.response.payload); +}; + +const formatUserError = (error: UserError): string => + error.kind === "auth-failed" ? error.authError.kind : error.sdkError.message; diff --git a/src/index.ts b/src/index.ts index 5986c97..3ec9147 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,62 @@ #!/usr/bin/env node -import yargs from 'yargs'; - -const createMigrationTool = (): number => { - yargs - .usage('The Kontent.ai CLI is a tool you can use for generating and running Kontent.ai migration scripts.') - .scriptName('kontent') - .commandDir('cmds') - .demandCommand(1, 'Please specify a command') - .wrap(null) - .help('h') - .alias('h', 'help') - .example('kontent', 'environment add --name DEV --environment-id --api-key ') - .example('kontent', 'migration add --name 02_my_migration') - - .example('kontent', 'migration run --name migration01 --environment-id --api-key ') - .example('kontent', 'migration run --name migration01 --environment DEV --debug true') - .example('kontent', 'migration run --all --environment DEV') - .example('kontent', 'migration run --range 1:4 --environment DEV') - - .example('kontent', 'backup --action backup --environment-id --api-key ') - .example('kontent', 'backup --action backup --environment ') - .example('kontent', 'backup --action restore --name backup_file --environment-id --api-key ') - .example('kontent', 'backup --action restore --name backup_file --environment --preserve-workflow false') - .example('kontent', 'backup --action clean --environment-id --api-key ') - .strict().argv; - - return 0; -}; - -createMigrationTool(); + +import chalk from "chalk"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { + createTelemetry, + formatTelemetryMode, + registerTelemetrySignalFlush, +} from "./lib/telemetry/tracking.js"; +import { addLogLevelOptions, logInfo } from "./log.js"; +import type { CommandDeps, RegisterCommand } from "./types/yargs.js"; + +const commandsToRegister: ReadonlyArray = [ + (await import("./commands/login/login.js")).register, + (await import("./commands/logout/logout.js")).register, + (await import("./commands/project/project.js")).register, + (await import("./commands/telemetry/telemetry.js")).register, +]; + +const emptyYargs = yargs(hideBin(process.argv)); + +const initialYargs = emptyYargs + .wrap(emptyYargs.terminalWidth()) + .env("KONTENT") + .scriptName("kontent") + .epilogue("Docs: https://kontent.ai/learn | Contact: devrel@kontent.ai") + .demandCommand(1, chalk.red("You need to provide a command to run.")) + .strict() + .config("configFile", "Path to a JSON file with CLI parameters.") + .help("h") + .alias("h", "help") + .alias("v", "version"); + +const withLogLevel = addLogLevelOptions(initialYargs); + +// Hidden options exist only so .strict() + .env("KONTENT") accept the +// KONTENT_* env vars; the resolvers read process.env directly. The auth0* ones +// are a developer escape hatch for pointing the CLI at a non-default tenant +// (e.g. QA) via KONTENT_AUTH0_* env vars. +const withHiddenEnvOptions = withLogLevel + .option("doNotTrack", { type: "boolean", hidden: true }) + .option("telemetryDebug", { type: "boolean", hidden: true }) + .option("url", { type: "string", hidden: true }) + .option("auth0Domain", { type: "string", hidden: true }) + .option("auth0ClientId", { type: "string", hidden: true }) + .option("auth0Audience", { type: "string", hidden: true }); + +const { telemetry, mode } = await createTelemetry(); +const deps: CommandDeps = { telemetry }; +registerTelemetrySignalFlush(telemetry); + +// Runs after parsing (so --verbose is known) and before the command handler. +const withTelemetryModeLog = withHiddenEnvOptions.middleware((args) => { + logInfo(args, "verbose", formatTelemetryMode(mode)); +}); + +await commandsToRegister + .reduce((current, register) => register(current, deps), withTelemetryModeLog) + .parseAsync(); + +await telemetry.flush(); diff --git a/src/lib/auth/config.ts b/src/lib/auth/config.ts new file mode 100644 index 0000000..5edac9a --- /dev/null +++ b/src/lib/auth/config.ts @@ -0,0 +1,23 @@ +export type Auth0Config = Readonly<{ + domain: string; + clientId: string; + audience?: string; + scope: string; +}>; + +// Baked at build time by tsdown from KONTENT_AUTH0_*; "" when unset. typeof +// guards the undeclared case (tsx). +declare const __AUTH0_DOMAIN__: string | undefined; +declare const __AUTH0_CLIENT_ID__: string | undefined; +declare const __AUTH0_AUDIENCE__: string | undefined; + +const bakedDomain = typeof __AUTH0_DOMAIN__ === "string" ? __AUTH0_DOMAIN__ : ""; +const bakedClientId = typeof __AUTH0_CLIENT_ID__ === "string" ? __AUTH0_CLIENT_ID__ : ""; +const bakedAudience = typeof __AUTH0_AUDIENCE__ === "string" ? __AUTH0_AUDIENCE__ : ""; + +export const getAuth0Config = (env: NodeJS.ProcessEnv = process.env): Auth0Config => ({ + domain: env.KONTENT_AUTH0_DOMAIN ?? bakedDomain, + clientId: env.KONTENT_AUTH0_CLIENT_ID ?? bakedClientId, + audience: env.KONTENT_AUTH0_AUDIENCE ?? bakedAudience, + scope: "openid profile email offline_access", +}); diff --git a/src/lib/auth/deviceFlow.ts b/src/lib/auth/deviceFlow.ts new file mode 100644 index 0000000..39172ac --- /dev/null +++ b/src/lib/auth/deviceFlow.ts @@ -0,0 +1,80 @@ +import { errors, Issuer } from "openid-client"; +import { match } from "ts-pattern"; + +import { err, isErr, ok, type Result, tryAsync } from "../result.js"; +import type { Auth0Config } from "./config.js"; +import { mapOpenIdTokensToTokenSet } from "./mapTokens.js"; +import type { AuthError, TokenSet } from "./types.js"; + +export type DeviceCodeInfo = Readonly<{ + userCode: string; + verificationUri: string; + verificationUriComplete: string; + expiresInSeconds: number; +}>; + +export type DeviceFlowDeps = Readonly<{ + onUserCode: (info: DeviceCodeInfo) => Promise; + nowMs: () => number; +}>; + +export const loginViaDeviceFlow = async ( + config: Auth0Config, + deps: DeviceFlowDeps, +): Promise> => { + const issuerResult = await tryAsync( + async () => Issuer.discover(`https://${config.domain}`), + (cause): AuthError => ({ kind: "discovery-failed", cause }), + ); + if (isErr(issuerResult)) { + return issuerResult; + } + + const client = new issuerResult.value.Client({ + client_id: config.clientId, + token_endpoint_auth_method: "none", + id_token_signed_response_alg: "RS256", + }); + + const handleResult = await tryAsync( + async () => client.deviceAuthorization({ scope: config.scope, audience: config.audience }), + (cause): AuthError => ({ kind: "device-auth-failed", cause }), + ); + if (isErr(handleResult)) { + return handleResult; + } + + const handle = handleResult.value; + await deps.onUserCode({ + userCode: handle.user_code, + verificationUri: handle.verification_uri, + verificationUriComplete: handle.verification_uri_complete, + expiresInSeconds: handle.expires_in, + }); + + try { + const polled = await handle.poll(); + const issuedAtMs = deps.nowMs(); + return ok(mapOpenIdTokensToTokenSet(polled, issuedAtMs)); + } catch (cause) { + return err(mapPollError(cause)); + } +}; + +const mapPollError = (cause: unknown): AuthError => { + if (!(cause instanceof errors.OPError)) { + return { kind: "unknown", cause }; + } + + return match(cause.error) + .with("access_denied", (): AuthError => ({ kind: "access-denied" })) + .with("expired_token", (): AuthError => ({ kind: "expired-token" })) + .with("slow_down", (): AuthError => ({ kind: "slow-down" })) + .otherwise( + (code): AuthError => ({ + kind: "poll-failed", + code: code ?? "unknown", + description: cause.error_description, + }), + ); +}; diff --git a/src/lib/auth/formatAuthError.ts b/src/lib/auth/formatAuthError.ts new file mode 100644 index 0000000..75b2a63 --- /dev/null +++ b/src/lib/auth/formatAuthError.ts @@ -0,0 +1,48 @@ +import { match } from "ts-pattern"; + +import type { AuthError } from "./types.js"; + +export const formatAuthError = (error: AuthError): string => + match(error) + .with({ kind: "not-logged-in" }, () => "Not logged in. Run `kontent login` first.") + .with({ kind: "access-denied" }, () => "login cancelled") + .with({ kind: "expired-token" }, () => "device flow expired") + .with({ kind: "slow-down" }, () => "polling rate limited; please retry") + .with( + { kind: "discovery-failed" }, + ({ cause }) => `failed to discover Auth0 issuer: ${describeCause(cause)}`, + ) + .with( + { kind: "device-auth-failed" }, + ({ cause }) => `failed to start device authorization: ${describeCause(cause)}`, + ) + .with( + { kind: "poll-failed" }, + ({ code, description }) => + `device flow failed (${code})${description ? `: ${description}` : ""}`, + ) + .with( + { kind: "refresh-failed" }, + ({ cause }) => `token refresh failed: ${describeCause(cause)}`, + ) + .with( + { kind: "storage-read-failed" }, + ({ cause }) => `failed to read stored tokens: ${describeCause(cause)}`, + ) + .with( + { kind: "storage-write-failed" }, + ({ cause }) => `failed to write stored tokens: ${describeCause(cause)}`, + ) + .with( + { kind: "storage-clear-failed" }, + ({ cause }) => `failed to clear stored tokens: ${describeCause(cause)}`, + ) + .with({ kind: "unknown" }, ({ cause }) => `unexpected error: ${describeCause(cause)}`) + .exhaustive(); + +const describeCause = (cause: unknown): string => { + if (cause instanceof Error) { + return cause.message; + } + return String(cause); +}; diff --git a/src/lib/auth/idTokenClaims.ts b/src/lib/auth/idTokenClaims.ts new file mode 100644 index 0000000..0cd115c --- /dev/null +++ b/src/lib/auth/idTokenClaims.ts @@ -0,0 +1,13 @@ +export const decodeIdTokenClaims = (idToken: string): Record => { + const parts = idToken.split("."); + const payload = parts[1]; + if (payload === undefined) { + return {}; + } + try { + const json = Buffer.from(payload, "base64url").toString("utf8"); + return JSON.parse(json) as Record; + } catch { + return {}; + } +}; diff --git a/src/lib/auth/logVerboseAuthInfo.ts b/src/lib/auth/logVerboseAuthInfo.ts new file mode 100644 index 0000000..986faa5 --- /dev/null +++ b/src/lib/auth/logVerboseAuthInfo.ts @@ -0,0 +1,44 @@ +import { inspect } from "node:util"; + +import { Issuer } from "openid-client"; + +import { isVerbose, type LogOptions, logInfo } from "../../log.js"; +import type { Auth0Config } from "./config.js"; +import { decodeIdTokenClaims } from "./idTokenClaims.js"; +import type { TokenSet } from "./types.js"; + +export const logVerboseAuthInfo = async ( + params: LogOptions, + config: Auth0Config, + tokens: TokenSet, +): Promise => { + if (!isVerbose(params)) { + return; + } + + logInfo(params, "verbose", `\n\nresult tokens ${inspectValue(tokens)}`); + + if (tokens.idToken !== undefined) { + logInfo( + params, + "verbose", + `\n\nID Token Claims ${inspectValue(decodeIdTokenClaims(tokens.idToken))}`, + ); + } + + try { + const issuer = await Issuer.discover(`https://${config.domain}`); + const client = new issuer.Client({ + client_id: config.clientId, + token_endpoint_auth_method: "none", + id_token_signed_response_alg: "RS256", + }); + const userInfo = await client.userinfo(tokens.accessToken); + logInfo(params, "verbose", `\n\nUserInfo response ${inspectValue(userInfo)}`); + } catch { + // userinfo errors are swallowed; access token may not be eligible for the userinfo endpoint + } +}; + +const inspectValue = (value: unknown): string => + inspect(value, { depth: null, colors: false, compact: false }); diff --git a/src/lib/auth/mapTokens.ts b/src/lib/auth/mapTokens.ts new file mode 100644 index 0000000..bd04de3 --- /dev/null +++ b/src/lib/auth/mapTokens.ts @@ -0,0 +1,22 @@ +import type { TokenSet as OpenIdTokenSet } from "openid-client"; + +import type { TokenSet } from "./types.js"; + +export const mapOpenIdTokensToTokenSet = ( + tokens: OpenIdTokenSet, + issuedAtMs: number, + fallbackRefreshToken?: string, +): TokenSet => { + if (tokens.access_token === undefined) { + throw new Error("openid-client returned no access_token"); + } + + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token ?? fallbackRefreshToken, + idToken: tokens.id_token, + expiresAt: tokens.expires_in !== undefined ? issuedAtMs + tokens.expires_in * 1000 : undefined, + scope: tokens.scope, + tokenType: tokens.token_type, + }; +}; diff --git a/src/lib/auth/refresh.ts b/src/lib/auth/refresh.ts new file mode 100644 index 0000000..78e6d58 --- /dev/null +++ b/src/lib/auth/refresh.ts @@ -0,0 +1,35 @@ +import { Issuer } from "openid-client"; + +import { isErr, ok, type Result, tryAsync } from "../result.js"; +import type { Auth0Config } from "./config.js"; +import { mapOpenIdTokensToTokenSet } from "./mapTokens.js"; +import type { AuthError, TokenSet } from "./types.js"; + +export const refreshTokens = async ( + config: Auth0Config, + refreshToken: string, +): Promise> => { + const discovered = await tryAsync( + async () => Issuer.discover(`https://${config.domain}`), + (cause): AuthError => ({ kind: "discovery-failed", cause }), + ); + if (isErr(discovered)) { + return discovered; + } + + const client = new discovered.value.Client({ + client_id: config.clientId, + token_endpoint_auth_method: "none", + id_token_signed_response_alg: "RS256", + }); + + const refreshed = await tryAsync( + async () => client.refresh(refreshToken), + (cause): AuthError => ({ kind: "refresh-failed", cause }), + ); + if (isErr(refreshed)) { + return refreshed; + } + + return ok(mapOpenIdTokensToTokenSet(refreshed.value, Date.now(), refreshToken)); +}; diff --git a/src/lib/auth/storage.ts b/src/lib/auth/storage.ts new file mode 100644 index 0000000..ee7e289 --- /dev/null +++ b/src/lib/auth/storage.ts @@ -0,0 +1,72 @@ +import { AsyncEntry } from "@napi-rs/keyring"; + +import { err, ok, type Result } from "../result.js"; +import type { AuthError, TokenSet } from "./types.js"; + +const SERVICE = "kontent-cli"; +const ACCOUNT = "default"; + +export type TokenStorage = Readonly<{ + read: () => Promise>; + write: (tokens: TokenSet) => Promise>; + clear: () => Promise>; +}>; + +export const createKeyringStorage = (): TokenStorage => { + const entry = new AsyncEntry(SERVICE, ACCOUNT); + + return { + read: async () => { + try { + const raw = await entry.getPassword(); + if (raw === undefined) { + return ok(null); + } + return ok(parseStoredTokens(raw)); + } catch (cause) { + return err({ kind: "storage-read-failed", cause }); + } + }, + + write: async (tokens) => { + try { + await entry.setPassword(JSON.stringify(tokens)); + return ok(undefined); + } catch (cause) { + return err({ kind: "storage-write-failed", cause }); + } + }, + + clear: async () => { + try { + const existing = await entry.getPassword(); + if (existing === undefined) { + return ok(undefined); + } + await entry.deleteCredential(); + return ok(undefined); + } catch (cause) { + return err({ kind: "storage-clear-failed", cause }); + } + }, + }; +}; + +const safeJsonParse = (raw: string): unknown => { + try { + return JSON.parse(raw); + } catch { + return null; + } +}; + +const parseStoredTokens = (raw: string): TokenSet | null => { + const parsed = safeJsonParse(raw); + return isTokenSetShape(parsed) ? parsed : null; +}; + +const isTokenSetShape = (value: unknown): value is TokenSet => + typeof value === "object" && + value !== null && + "accessToken" in value && + typeof value.accessToken === "string"; diff --git a/src/lib/auth/tokenAccess.ts b/src/lib/auth/tokenAccess.ts new file mode 100644 index 0000000..ad8e83b --- /dev/null +++ b/src/lib/auth/tokenAccess.ts @@ -0,0 +1,77 @@ +import { match } from "ts-pattern"; + +import { err, isErr, ok, type Result } from "../result.js"; +import { type Auth0Config, getAuth0Config } from "./config.js"; +import { refreshTokens } from "./refresh.js"; +import { createKeyringStorage, type TokenStorage } from "./storage.js"; +import type { AuthDecision, AuthError, TokenSet } from "./types.js"; + +export const EXPIRY_SKEW_MS = 60_000; + +export const decideAuth = (tokens: TokenSet | null, nowMs: number): AuthDecision => { + if (tokens === null) { + return { type: "login" }; + } + + const isAccessTokenLive = + tokens.expiresAt === undefined || tokens.expiresAt - EXPIRY_SKEW_MS > nowMs; + + if (isAccessTokenLive) { + return { type: "use-existing-token", accessToken: tokens.accessToken }; + } + + if (tokens.refreshToken !== undefined) { + return { type: "refresh-token", refreshToken: tokens.refreshToken }; + } + + return { type: "login" }; +}; + +// Refreshes the token set, clearing stored tokens if the refresh fails so a +// stale refresh token can't get retried forever. +export const refreshOrClear = async ( + storage: TokenStorage, + config: Auth0Config, + refreshToken: string, +): Promise> => { + const refreshed = await refreshTokens(config, refreshToken); + if (isErr(refreshed)) { + await storage.clear(); + } + return refreshed; +}; + +export const getValidAccessToken = async (): Promise> => { + const storage = createKeyringStorage(); + const stored = await storage.read(); + if (isErr(stored)) { + return stored; + } + + const decision = decideAuth(stored.value, Date.now()); + + return await match(decision) + .with( + { type: "use-existing-token" }, + async ({ accessToken }): Promise> => ok(accessToken), + ) + .with( + { type: "login" }, + async (): Promise> => err({ kind: "not-logged-in" }), + ) + .with( + { type: "refresh-token" }, + async ({ refreshToken }): Promise> => { + const refreshed = await refreshOrClear(storage, getAuth0Config(), refreshToken); + if (isErr(refreshed)) { + return refreshed; + } + const written = await storage.write(refreshed.value); + if (isErr(written)) { + return written; + } + return ok(refreshed.value.accessToken); + }, + ) + .exhaustive(); +}; diff --git a/src/lib/auth/types.ts b/src/lib/auth/types.ts new file mode 100644 index 0000000..907056f --- /dev/null +++ b/src/lib/auth/types.ts @@ -0,0 +1,27 @@ +export type TokenSet = Readonly<{ + accessToken: string; + refreshToken?: string; + idToken?: string; + expiresAt?: number; + scope?: string; + tokenType?: string; +}>; + +export type AuthDecision = + | { readonly type: "use-existing-token"; readonly accessToken: string } + | { readonly type: "refresh-token"; readonly refreshToken: string } + | { readonly type: "login" }; + +export type AuthError = + | { readonly kind: "not-logged-in" } + | { readonly kind: "access-denied" } + | { readonly kind: "expired-token" } + | { readonly kind: "slow-down" } + | { readonly kind: "discovery-failed"; readonly cause: unknown } + | { readonly kind: "device-auth-failed"; readonly cause: unknown } + | { readonly kind: "poll-failed"; readonly code: string; readonly description?: string } + | { readonly kind: "refresh-failed"; readonly cause: unknown } + | { readonly kind: "storage-read-failed"; readonly cause: unknown } + | { readonly kind: "storage-write-failed"; readonly cause: unknown } + | { readonly kind: "storage-clear-failed"; readonly cause: unknown } + | { readonly kind: "unknown"; readonly cause: unknown }; diff --git a/src/lib/config/cliConfig.ts b/src/lib/config/cliConfig.ts new file mode 100644 index 0000000..06f4cb7 --- /dev/null +++ b/src/lib/config/cliConfig.ts @@ -0,0 +1,73 @@ +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { errorMessage } from "../error.js"; +import { err, ok, type Result } from "../result.js"; + +export type CliConfig = Readonly<{ + telemetryEnabled?: boolean; + telemetryNoticeShown?: boolean; + userId?: string; + deviceId?: string; +}>; + +export const getCliConfigPath = (): string => + path.join(resolveConfigBaseDir(), "kontent-cli", "config.json"); + +const resolveConfigBaseDir = (): string => { + if (process.env.XDG_CONFIG_HOME) { + return process.env.XDG_CONFIG_HOME; + } + if (process.platform === "win32" && process.env.APPDATA) { + return process.env.APPDATA; + } + return path.join(os.homedir(), ".config"); +}; + +export const readCliConfig = async (): Promise => { + try { + const raw = await readFile(getCliConfigPath(), "utf8"); + return parseCliConfig(JSON.parse(raw)); + } catch { + return {}; + } +}; + +const parseCliConfig = (parsed: unknown): CliConfig => { + if (typeof parsed !== "object" || parsed === null) { + return {}; + } + const raw = parsed as Record; + return { + ...(typeof raw.telemetryEnabled === "boolean" + ? { telemetryEnabled: raw.telemetryEnabled } + : {}), + ...(typeof raw.telemetryNoticeShown === "boolean" + ? { telemetryNoticeShown: raw.telemetryNoticeShown } + : {}), + ...(typeof raw.userId === "string" ? { userId: raw.userId } : {}), + ...(typeof raw.deviceId === "string" ? { deviceId: raw.deviceId } : {}), + }; +}; + +export const writeCliConfig = async (patch: Partial): Promise> => { + const configPath = getCliConfigPath(); + try { + await mkdir(path.dirname(configPath), { recursive: true }); + const current = await readCliConfig(); + const next = { ...current, ...patch }; + await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, { + encoding: "utf8", + mode: 0o600, + }); + // writeFile applies `mode` only when it creates the file; if the file + // already exists with looser permissions (created by hand, or by a tool + // that did not set a mode), the write above would silently keep them. + // The explicit chmod makes owner-only access hold after every write. + await chmod(configPath, 0o600); + return ok(undefined); + } catch (cause) { + return err(errorMessage(cause)); + } +}; diff --git a/src/lib/config/iapiUrl.ts b/src/lib/config/iapiUrl.ts new file mode 100644 index 0000000..979677f --- /dev/null +++ b/src/lib/config/iapiUrl.ts @@ -0,0 +1,8 @@ +import type { BaseUrl } from "@kontent-ai/core-sdk"; + +import { kontentAppHost } from "./kontentUrl.js"; + +export const iapiBaseUrl: BaseUrl = { + protocol: "https", + host: kontentAppHost(), +}; diff --git a/src/lib/config/kontentUrl.ts b/src/lib/config/kontentUrl.ts new file mode 100644 index 0000000..a31e2dc --- /dev/null +++ b/src/lib/config/kontentUrl.ts @@ -0,0 +1,13 @@ +declare const __KONTENT_URL__: string | undefined; + +const DEFAULT_KONTENT_DOMAIN = "kontent.ai"; + +const bakedDomain = + typeof __KONTENT_URL__ === "string" && __KONTENT_URL__ !== "" ? __KONTENT_URL__ : undefined; + +const getKontentBaseDomain = (): string => + process.env.KONTENT_URL ?? bakedDomain ?? DEFAULT_KONTENT_DOMAIN; + +export const kontentAppHost = (): string => `app.${getKontentBaseDomain()}`; + +export const kontentManagementUrl = (): string => `https://manage.${getKontentBaseDomain()}/v2`; diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..8b501fe --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,2 @@ +export const isTruthyEnv = (value: string | undefined): boolean => + value !== undefined && !["", "0", "false", "no"].includes(value.toLowerCase()); diff --git a/src/lib/envFile.ts b/src/lib/envFile.ts new file mode 100644 index 0000000..77c65d6 --- /dev/null +++ b/src/lib/envFile.ts @@ -0,0 +1,25 @@ +const keyOfLine = (line: string): string | null => { + if (!line.includes("=")) { + return null; + } + const [name = ""] = line.split("="); + return name.trim(); +}; + +/** + * Fills a `.env`-style template with `values`: a `KEY=...` line whose key is in + * `values` has its value replaced in place; comments, blank lines, and keys we + * don't set are kept verbatim. Templates are the source of truth for which keys + * exist, so a value whose key isn't in the template is simply ignored. + */ +export const applyEnvOverrides = ( + template: string, + values: Readonly>, +): string => + template + .split("\n") + .map((line) => { + const key = keyOfLine(line); + return key !== null && Object.hasOwn(values, key) ? `${key}=${values[key]}` : line; + }) + .join("\n"); diff --git a/src/lib/error.ts b/src/lib/error.ts new file mode 100644 index 0000000..7e6cb17 --- /dev/null +++ b/src/lib/error.ts @@ -0,0 +1,39 @@ +import { SharedModels } from "@kontent-ai/management-sdk"; + +export const errorMessage = (cause: unknown): string => + cause instanceof Error ? cause.message : String(cause); + +export const mapiErrorMessage = (cause: unknown): string => { + if (!(cause instanceof SharedModels.ContentManagementBaseKontentError)) { + return errorMessage(cause); + } + + return JSON.stringify( + { + message: cause.message, + errorCode: cause.errorCode, + validationErrors: [...new Set(cause.validationErrors.map((error) => error.message))], + requestId: cause.requestId, + ...requestInfo(cause.originalError), + }, + null, + 2, + ); +}; + +const requestInfo = ( + originalError: unknown, +): { method?: string; url?: string; status?: number } => { + if (typeof originalError !== "object" || originalError === null) { + return {}; + } + const axiosError = originalError as { + config?: { method?: string; url?: string }; + response?: { status?: number }; + }; + return { + method: axiosError.config?.method?.toUpperCase(), + url: axiosError.config?.url, + status: axiosError.response?.status, + }; +}; diff --git a/src/lib/iapi/client.ts b/src/lib/iapi/client.ts new file mode 100644 index 0000000..eae6387 --- /dev/null +++ b/src/lib/iapi/client.ts @@ -0,0 +1,58 @@ +import { + type BaseUrl, + getDefaultHttpService, + type KontentSdkError, + type SdkConfig, + type SdkInfo, +} from "@kontent-ai/core-sdk"; + +// biome-ignore lint/correctness/useImportExtensions: JSON imports must keep the .json extension +import pkg from "../../../package.json" with { type: "json" }; +import { iapiBaseUrl } from "../config/iapiUrl.js"; + +const iapiSdkInfo: SdkInfo = { + name: pkg.name, + version: pkg.version, + host: "npmjs.com", +}; + +export const iapiEndpointUrl = (base: BaseUrl, path: string): URL => + new URL(path, `${base.protocol}://${base.host}`); + +export type IapiClient = Readonly<{ + config: SdkConfig; + sdkInfo: SdkInfo; + urlBase: BaseUrl; + token: string; +}>; + +export const createIapiClient = ({ + token, + baseUrl = iapiBaseUrl, +}: { + readonly token: string; + readonly baseUrl?: BaseUrl; +}): IapiClient => ({ + config: { + baseUrl, + httpService: getDefaultHttpService({ + requestHeaders: [{ name: "Origin", value: `${baseUrl.protocol}://${baseUrl.host}` }], + retryStrategy: { + maxRetries: 3, + canRetryAdapterError: () => true, + }, + }), + }, + sdkInfo: iapiSdkInfo, + urlBase: baseUrl, + token, +}); + +export const iapiQueryBase = (c: IapiClient) => ({ + config: c.config, + sdkInfo: c.sdkInfo, + authorizationApiKey: c.token, + mapError: (e: KontentSdkError) => e, + mapMetadata: () => ({}), + mapExtraResponseProps: () => ({}), +}); diff --git a/src/lib/iapi/endpoints/createDeliveryApiKey.ts b/src/lib/iapi/endpoints/createDeliveryApiKey.ts new file mode 100644 index 0000000..689aad5 --- /dev/null +++ b/src/lib/iapi/endpoints/createDeliveryApiKey.ts @@ -0,0 +1,34 @@ +import { createMutationQuery } from "@kontent-ai/core-sdk"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; +import { ApiKeyDetailSchema } from "./getApiKeyDetail.js"; + +export type CreateDeliveryApiKeyInput = Readonly<{ + name: string; + environments: ReadonlyArray; + hasPreviewDeliveryAccess: boolean; + expiresAt: Date; +}>; + +export const createDeliveryApiKey = ( + c: IapiClient, + containerId: string, + input: CreateDeliveryApiKeyInput, +) => + createMutationQuery({ + method: "POST", + url: iapiEndpointUrl(c.urlBase, `/api/project-container/${containerId}/keys`), + body: { + name: input.name, + shared_with_users: [], + has_secure_delivery_access: false, + has_preview_delivery_access: input.hasPreviewDeliveryAccess, + has_access_to_all_environments: false, + environments: input.environments, + management_api_key_capabilities: null, + expires_at: input.expiresAt.toISOString(), + type: "delivery-api", + }, + schema: ApiKeyDetailSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/iapi/endpoints/getApiKeyDetail.ts b/src/lib/iapi/endpoints/getApiKeyDetail.ts new file mode 100644 index 0000000..0087ee8 --- /dev/null +++ b/src/lib/iapi/endpoints/getApiKeyDetail.ts @@ -0,0 +1,28 @@ +import { createFetchQuery, jsonValueSchema } from "@kontent-ai/core-sdk"; +import * as z from "zod/mini"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; +import { ApiKeyTypeSchema } from "./listApiKeys.js"; + +export const ApiKeyDetailSchema = z.object({ + token_seed_id: z.string(), + api_key: z.string(), + name: z.string(), + type: ApiKeyTypeSchema, + shared_with_users: z.array(z.string()), + has_secure_delivery_access: z.boolean(), + has_preview_delivery_access: z.boolean(), + has_access_to_all_environments: z.boolean(), + environments: z.array(z.string()), + expires_at: z.string(), + management_api_key_capabilities: jsonValueSchema, +}); + +export type ApiKeyDetail = z.infer; + +export const getApiKeyDetail = (c: IapiClient, containerId: string, tokenSeedId: string) => + createFetchQuery({ + url: iapiEndpointUrl(c.urlBase, `/api/project-container/${containerId}/keys/${tokenSeedId}`), + schema: ApiKeyDetailSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/iapi/endpoints/getProjectInfo.ts b/src/lib/iapi/endpoints/getProjectInfo.ts new file mode 100644 index 0000000..254d7a1 --- /dev/null +++ b/src/lib/iapi/endpoints/getProjectInfo.ts @@ -0,0 +1,29 @@ +import { createFetchQuery } from "@kontent-ai/core-sdk"; +import * as z from "zod/mini"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; + +export const ProjectResponseSchema = z.object({ + projectGuid: z.string(), + projectName: z.string(), + projectContainerId: z.string(), + projectContainerName: z.string(), + projectContainerMasterProjectId: z.string(), + inactive: z.boolean(), + deactivatedAt: z.nullable(z.string()), + activatedAt: z.nullable(z.string()), + createdAt: z.string(), + productionFrom: z.nullable(z.string()), + productionTo: z.nullable(z.string()), + subscriptionId: z.string(), + projectLocationId: z.string(), +}); + +export type ProjectResponse = z.infer; + +export const getProjectInfo = (c: IapiClient, environmentId: string) => + createFetchQuery({ + url: iapiEndpointUrl(c.urlBase, `/api/project-management/${environmentId}`), + schema: ProjectResponseSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/iapi/endpoints/getUser.ts b/src/lib/iapi/endpoints/getUser.ts new file mode 100644 index 0000000..1ecc1e1 --- /dev/null +++ b/src/lib/iapi/endpoints/getUser.ts @@ -0,0 +1,19 @@ +import { createFetchQuery } from "@kontent-ai/core-sdk"; +import * as z from "zod/mini"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; + +export const UserInfoSchema = z.object({ + userId: z.string(), + isEmailVerified: z.optional(z.boolean()), + hadTrial: z.optional(z.boolean()), +}); + +export type UserInfo = z.infer; + +export const getUser = (c: IapiClient) => + createFetchQuery({ + url: iapiEndpointUrl(c.urlBase, "/api/user"), + schema: UserInfoSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/iapi/endpoints/listApiKeys.ts b/src/lib/iapi/endpoints/listApiKeys.ts new file mode 100644 index 0000000..d5e4183 --- /dev/null +++ b/src/lib/iapi/endpoints/listApiKeys.ts @@ -0,0 +1,52 @@ +import { createMutationQuery } from "@kontent-ai/core-sdk"; +import * as z from "zod/mini"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; + +export const ApiKeyTypeSchema = z.enum([ + "unknown", + "delivery-api", + "management-api-pat", + "management-api", + "subscription-api", + "web-spotlight-api", + "delivery-api-primary", + "delivery-api-secondary", + "preview-delivery-api-primary", + "preview-delivery-api-secondary", +]); + +export type ApiKeyType = z.infer; + +export const ApiKeyListingItemSchema = z.object({ + token_seed_id: z.string(), + name: z.string(), + userId: z.string(), + type: ApiKeyTypeSchema, + has_access_to_all_environments: z.boolean(), + environments: z.array(z.string()), + expires_at: z.string(), +}); + +export const ApiKeyListingSchema = z.array(ApiKeyListingItemSchema); + +export type ApiKeyListingItem = z.infer; + +export type ListApiKeysFilter = Readonly<{ + query?: string; + apiKeyTypes?: ReadonlyArray; + environments?: ReadonlyArray; +}>; + +export const listApiKeys = (c: IapiClient, containerId: string, filter: ListApiKeysFilter = {}) => + createMutationQuery({ + method: "POST", + url: iapiEndpointUrl(c.urlBase, `/api/project-container/${containerId}/keys/listing`), + body: { + query: filter.query ?? null, + api_key_types: filter.apiKeyTypes ?? null, + environments: filter.environments ?? null, + }, + schema: ApiKeyListingSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/iapi/endpoints/listProjectProperties.ts b/src/lib/iapi/endpoints/listProjectProperties.ts new file mode 100644 index 0000000..c915984 --- /dev/null +++ b/src/lib/iapi/endpoints/listProjectProperties.ts @@ -0,0 +1,21 @@ +import { createFetchQuery } from "@kontent-ai/core-sdk"; +import * as z from "zod/mini"; + +import { type IapiClient, iapiEndpointUrl, iapiQueryBase } from "../client.js"; + +export const ProjectPropertySchema = z.object({ + projectId: z.string(), + key: z.string(), + value: z.string(), +}); + +export const ProjectPropertyListSchema = z.array(ProjectPropertySchema); + +export type ProjectProperty = z.infer; + +export const listProjectProperties = (c: IapiClient, environmentId: string) => + createFetchQuery({ + url: iapiEndpointUrl(c.urlBase, `/api/project/${environmentId}/property`), + schema: ProjectPropertyListSchema, + ...iapiQueryBase(c), + }); diff --git a/src/lib/mapi/client.ts b/src/lib/mapi/client.ts new file mode 100644 index 0000000..05f6da3 --- /dev/null +++ b/src/lib/mapi/client.ts @@ -0,0 +1,20 @@ +import { HttpService } from "@kontent-ai/core-sdk-v10"; +import { createManagementClient, type ManagementClient } from "@kontent-ai/management-sdk"; + +import { kontentManagementUrl } from "../config/kontentUrl.js"; + +export type MapiClient = ManagementClient; + +export const createMapiClient = ({ + token, + envId, +}: { + readonly token: string; + readonly envId: string; +}): MapiClient => + createManagementClient({ + environmentId: envId, + apiKey: token, + baseUrl: kontentManagementUrl(), + httpService: new HttpService({ logErrorsToConsole: false }), + }); diff --git a/src/lib/option.ts b/src/lib/option.ts new file mode 100644 index 0000000..8af1bd9 --- /dev/null +++ b/src/lib/option.ts @@ -0,0 +1,35 @@ +export type Option = { readonly kind: "some"; readonly value: T } | { readonly kind: "none" }; + +export const some = (value: T): Option => ({ kind: "some", value }); + +export const none: Option = { kind: "none" }; + +export const fromNullable = (value: T | null | undefined): Option => + value === null || value === undefined ? none : some(value); + +export const isSome = (opt: Option): opt is { readonly kind: "some"; readonly value: T } => + opt.kind === "some"; + +export const isNone = (opt: Option): opt is { readonly kind: "none" } => opt.kind === "none"; + +export const map = (opt: Option, fn: (value: T) => U): Option => + opt.kind === "some" ? some(fn(opt.value)) : opt; + +export const flatMap = (opt: Option, fn: (value: T) => Option): Option => + opt.kind === "some" ? fn(opt.value) : opt; + +export const filter = (opt: Option, predicate: (value: T) => boolean): Option => + opt.kind === "some" && predicate(opt.value) ? opt : none; + +export const orElse = (opt: Option, fn: () => Option): Option => + opt.kind === "some" ? opt : fn(); + +export const getOrElse = (opt: Option, fn: () => T): T => + opt.kind === "some" ? opt.value : fn(); + +export const match = ( + opt: Option, + handlers: { some: (value: T) => U; none: () => U }, +): U => (opt.kind === "some" ? handlers.some(opt.value) : handlers.none()); + +export const toNullable = (opt: Option): T | null => (opt.kind === "some" ? opt.value : null); diff --git a/src/lib/result.ts b/src/lib/result.ts new file mode 100644 index 0000000..4210b94 --- /dev/null +++ b/src/lib/result.ts @@ -0,0 +1,68 @@ +export type Result = + | { readonly kind: "ok"; readonly value: T } + | { readonly kind: "err"; readonly error: E }; + +export const ok = (value: T): Result => ({ kind: "ok", value }); + +export const err = (error: E): Result => ({ kind: "err", error }); + +export const fromThrowable = (fn: () => T, onError: (cause: unknown) => E): Result => { + try { + return ok(fn()); + } catch (cause) { + return err(onError(cause)); + } +}; + +export const tryAsync = async ( + fn: () => Promise, + onError: (cause: unknown) => E, +): Promise> => { + try { + return ok(await fn()); + } catch (cause) { + return err(onError(cause)); + } +}; + +export const isOk = (res: Result): res is { readonly kind: "ok"; readonly value: T } => + res.kind === "ok"; + +export const isErr = ( + res: Result, +): res is { readonly kind: "err"; readonly error: E } => res.kind === "err"; + +export const map = (res: Result, fn: (value: T) => U): Result => + res.kind === "ok" ? ok(fn(res.value)) : res; + +export const mapErr = (res: Result, fn: (error: E) => F): Result => + res.kind === "err" ? err(fn(res.error)) : res; + +export const flatMap = ( + res: Result, + fn: (value: T) => Result, +): Result => (res.kind === "ok" ? fn(res.value) : res); + +export const filter = ( + res: Result, + predicate: (value: T) => boolean, + onFalse: (value: T) => E, +): Result => flatMap(res, (value) => (predicate(value) ? ok(value) : err(onFalse(value)))); + +export const orElse = (res: Result, fn: (error: E) => Result): Result => + res.kind === "ok" ? res : fn(res.error); + +export const getOrElse = (res: Result, fn: (error: E) => T): T => + res.kind === "ok" ? res.value : fn(res.error); + +export const partition = ( + results: ReadonlyArray>, +): { values: T[]; errors: E[] } => ({ + values: results.flatMap((res) => (res.kind === "ok" ? [res.value] : [])), + errors: results.flatMap((res) => (res.kind === "err" ? [res.error] : [])), +}); + +export const combine = (results: ReadonlyArray>): Result => { + const { values, errors } = partition(results); + return errors.length === 0 ? ok(values) : err(errors); +}; diff --git a/src/lib/telemetry/consent.ts b/src/lib/telemetry/consent.ts new file mode 100644 index 0000000..d47cf25 --- /dev/null +++ b/src/lib/telemetry/consent.ts @@ -0,0 +1,51 @@ +import { match } from "ts-pattern"; + +import type { CliConfig } from "../config/cliConfig.js"; +import { isTruthyEnv } from "../env.js"; + +export type TelemetryOffReason = + | "do-not-track-env" + | "kontent-do-not-track-env" + | "config-disabled" + | "ci" + | "missing-api-key"; + +export type TelemetryConsent = + | { readonly isEnabled: true } + | { readonly isEnabled: false; readonly reason: TelemetryOffReason }; + +export const isTelemetryDebug = (env: NodeJS.ProcessEnv): boolean => + isTruthyEnv(env.KONTENT_TELEMETRY_DEBUG); + +export const resolveTelemetryConsent = ( + env: NodeJS.ProcessEnv, + config: CliConfig, + amplitudeApiKey: string, + isCiRun: boolean, +): TelemetryConsent => { + if (isTruthyEnv(env.DO_NOT_TRACK)) { + return { isEnabled: false, reason: "do-not-track-env" }; + } + if (isTruthyEnv(env.KONTENT_DO_NOT_TRACK)) { + return { isEnabled: false, reason: "kontent-do-not-track-env" }; + } + if (config.telemetryEnabled === false) { + return { isEnabled: false, reason: "config-disabled" }; + } + if (isCiRun) { + return { isEnabled: false, reason: "ci" }; + } + if (amplitudeApiKey === "") { + return { isEnabled: false, reason: "missing-api-key" }; + } + return { isEnabled: true }; +}; + +export const formatTelemetryOffReason = (reason: TelemetryOffReason): string => + match(reason) + .with("do-not-track-env", () => "DO_NOT_TRACK environment variable is set") + .with("kontent-do-not-track-env", () => "KONTENT_DO_NOT_TRACK environment variable is set") + .with("config-disabled", () => "disabled in the config file") + .with("ci", () => "CI environment detected") + .with("missing-api-key", () => "this build has no telemetry API key") + .exhaustive(); diff --git a/src/lib/telemetry/context.ts b/src/lib/telemetry/context.ts new file mode 100644 index 0000000..1371795 --- /dev/null +++ b/src/lib/telemetry/context.ts @@ -0,0 +1,12 @@ +// biome-ignore lint/correctness/useImportExtensions: importing package.json, not a TS module +import pkg from "../../../package.json" with { type: "json" }; + +declare const __AMPLITUDE_API_KEY__: string | undefined; + +// The constant is injected by tsdown's define at build time; typeof on an +// undeclared identifier never throws, so unbundled runs (tsc, tsx) safely fall +// back to an empty key and send nothing. +export const amplitudeApiKey: string = + typeof __AMPLITUDE_API_KEY__ === "string" ? __AMPLITUDE_API_KEY__ : ""; + +export const cliVersion: string = pkg.version; diff --git a/src/lib/telemetry/events.ts b/src/lib/telemetry/events.ts new file mode 100644 index 0000000..f738d8f --- /dev/null +++ b/src/lib/telemetry/events.ts @@ -0,0 +1,11 @@ +export type CommandOutcome = "success" | "error"; + +export type EventProperties = Record; + +export type TelemetryEvent = Readonly<{ + name: string; + properties: EventProperties; +}>; + +export const toEventType = (command: string): string => + `cli__${command.trim().replace(/\s+/g, "-")}`; diff --git a/src/lib/telemetry/identity.ts b/src/lib/telemetry/identity.ts new file mode 100644 index 0000000..d1adb63 --- /dev/null +++ b/src/lib/telemetry/identity.ts @@ -0,0 +1,35 @@ +import { randomUUID } from "node:crypto"; + +import { readCliConfig, writeCliConfig } from "../config/cliConfig.js"; + +export type TelemetryIdentity = Readonly<{ + deviceId: string; + userId?: string; +}>; + +// Amplitude rejects user/device ids shorter than 5 characters by default +const MIN_ID_LENGTH = 5; + +export const resolveIdentity = async (): Promise => { + const deviceId = await resolveDeviceId(); + const userId = await resolveUserId(); + return userId === null ? { deviceId } : { deviceId, userId }; +}; + +const isValidId = (value: unknown): value is string => + typeof value === "string" && value.length >= MIN_ID_LENGTH; + +const resolveDeviceId = async (): Promise => { + const existing = (await readCliConfig()).deviceId; + if (isValidId(existing)) { + return existing; + } + const deviceId = randomUUID(); + await writeCliConfig({ deviceId }); + return deviceId; +}; + +const resolveUserId = async (): Promise => { + const cachedUserId = (await readCliConfig()).userId; + return isValidId(cachedUserId) ? cachedUserId : null; +}; diff --git a/src/lib/telemetry/sink.ts b/src/lib/telemetry/sink.ts new file mode 100644 index 0000000..8490182 --- /dev/null +++ b/src/lib/telemetry/sink.ts @@ -0,0 +1,70 @@ +import os from "node:os"; + +import { createInstance, Types } from "@amplitude/analytics-node"; + +import { cliVersion } from "./context.js"; +import type { TelemetryEvent } from "./events.js"; +import type { TelemetryIdentity } from "./identity.js"; + +export type TrackOutcome = + | { readonly kind: "accepted"; readonly code: number } + | { readonly kind: "rejected"; readonly code: number; readonly message: string } + | { readonly kind: "skipped" }; + +export type TelemetrySink = Readonly<{ + track: (event: TelemetryEvent, identity: TelemetryIdentity) => Promise; + flush: () => Promise; +}>; + +export const createDebugTelemetrySink = (): TelemetrySink => ({ + // biome-ignore lint/suspicious/useAwait: async required by the TelemetrySink interface + track: async (event, identity) => { + const line = JSON.stringify({ + name: event.name, + identity, + properties: event.properties, + }); + process.stderr.write(`[telemetry debug] ${line}\n`); + return { kind: "skipped" }; + }, + flush: async () => {}, +}); + +export const createAmplitudeSink = (apiKey: string): TelemetrySink => { + const client = createInstance(); + // Not awaited on purpose: events tracked before init resolves are queued internally. + client.init(apiKey, { + // TODO: switch back to "EU" once the production Amplitude project exists + // in the EU data center; the current test project is in the US. + serverZone: "US", + // The CLI process is short-lived; the default thresholds (300 events / 10 s) + // would never fire before exit. + flushQueueSize: 1, + flushIntervalMillis: 1000, + flushMaxRetries: 1, + logLevel: Types.LogLevel.None, + }); + + return { + track: async (event, identity) => { + const result = await client.track(event.name, event.properties, toEventOptions(identity)) + .promise; + if (result.code === 200) { + return { kind: "accepted", code: result.code }; + } + return { kind: "rejected", code: result.code, message: result.message }; + }, + flush: async () => { + await client.flush().promise; + }, + }; +}; + +const toEventOptions = (identity: TelemetryIdentity): Types.EventOptions => ({ + device_id: identity.deviceId, + ...(identity.userId !== undefined ? { user_id: identity.userId } : {}), + platform: "CLI", + app_version: cliVersion, + os_name: os.platform(), + os_version: os.release(), +}); diff --git a/src/lib/telemetry/tracking.ts b/src/lib/telemetry/tracking.ts new file mode 100644 index 0000000..9487072 --- /dev/null +++ b/src/lib/telemetry/tracking.ts @@ -0,0 +1,189 @@ +import { isCI } from "ci-info"; +import { match } from "ts-pattern"; +import { type LogOptions, logInfo } from "../../log.js"; +import { readCliConfig, writeCliConfig } from "../config/cliConfig.js"; +import { + formatTelemetryOffReason, + isTelemetryDebug, + resolveTelemetryConsent, + type TelemetryOffReason, +} from "./consent.js"; +import { amplitudeApiKey } from "./context.js"; +import { type CommandOutcome, type EventProperties, toEventType } from "./events.js"; +import { resolveIdentity } from "./identity.js"; +import { + createAmplitudeSink, + createDebugTelemetrySink, + type TelemetrySink, + type TrackOutcome, +} from "./sink.js"; + +const FLUSH_TIMEOUT_MS = 1500; + +const FIRST_RUN_NOTICE = `kontent-cli sends anonymous usage telemetry (command name, success/failure, +duration) to help improve it. Opt out: kontent telemetry disable`; + +export type CommandTracker = Readonly<{ + succeed: (props?: EventProperties) => void; + fail: (errorCode: string, props?: EventProperties) => void; +}>; + +export type Telemetry = Readonly<{ + startCommandTracking: (command: string, params: LogOptions) => CommandTracker; + flush: () => Promise; +}>; + +export type TelemetryMode = + | { readonly kind: "live" } + | { readonly kind: "disabled"; readonly reason: TelemetryOffReason } + | { readonly kind: "notice-run" } + | { readonly kind: "debug" } + | { readonly kind: "init-failed" }; + +export type TelemetryInit = Readonly<{ + telemetry: Telemetry; + mode: TelemetryMode; +}>; + +const noopTracker: CommandTracker = { + succeed: () => {}, + fail: () => {}, +}; + +export const noopTelemetry: Telemetry = { + startCommandTracking: () => noopTracker, + flush: async () => {}, +}; + +export const createTelemetry = async (): Promise => { + try { + const { sink, mode } = await resolveSink(); + if (sink === null) { + return { telemetry: noopTelemetry, mode }; + } + + const telemetry: Telemetry = { + startCommandTracking: (command, params) => { + const startedAtMs = Date.now(); + let hasFinished = false; + + const finish = ( + outcome: CommandOutcome, + errorCode?: string, + props?: EventProperties, + ): void => { + if (hasFinished) { + return; + } + hasFinished = true; + void resolveIdentity() + .then(async (identity) => + sink.track( + { + name: toEventType(command), + properties: { + outcome, + "error-code": errorCode, + "duration-ms": Date.now() - startedAtMs, + ...props, + }, + }, + identity, + ), + ) + .then((trackOutcome) => { + if (trackOutcome.kind !== "skipped") { + logInfo(params, "verbose", formatTrackOutcome(trackOutcome)); + } + }) + .catch(() => { + // telemetry must never affect the command + }); + }; + + return { + succeed: (props) => finish("success", undefined, props), + fail: (errorCode, props) => finish("error", errorCode, props), + }; + }, + + flush: async () => { + try { + await Promise.race([sink.flush(), delay(FLUSH_TIMEOUT_MS)]); + } catch { + // never block or fail exit because of telemetry + } + }, + }; + + return { telemetry, mode }; + } catch { + return { telemetry: noopTelemetry, mode: { kind: "init-failed" } }; + } +}; + +type SinkResolution = Readonly<{ + sink: TelemetrySink | null; + mode: TelemetryMode; +}>; + +const resolveSink = async (): Promise => { + const config = await readCliConfig(); + const consent = resolveTelemetryConsent(process.env, config, amplitudeApiKey, isCI); + + if (isTelemetryDebug(process.env)) { + const decisionText = consent.isEnabled ? "enabled" : `disabled (${consent.reason})`; + process.stderr.write(`[telemetry debug] decision: ${decisionText}; nothing will be sent\n`); + return { sink: createDebugTelemetrySink(), mode: { kind: "debug" } }; + } + + if (!consent.isEnabled) { + return { sink: null, mode: { kind: "disabled", reason: consent.reason } }; + } + + if (config.telemetryNoticeShown !== true) { + process.stderr.write(`${FIRST_RUN_NOTICE}\n\n`); + // Best effort: if the write fails, the notice shows again next run and + // telemetry keeps sending nothing. + await writeCliConfig({ telemetryNoticeShown: true }); + return { sink: null, mode: { kind: "notice-run" } }; + } + + return { sink: createAmplitudeSink(amplitudeApiKey), mode: { kind: "live" } }; +}; + +export const formatTelemetryMode = (mode: TelemetryMode): string => + match(mode) + .with({ kind: "live" }, () => "Telemetry: enabled") + .with( + { kind: "disabled" }, + (m) => `Telemetry: disabled (${formatTelemetryOffReason(m.reason)})`, + ) + .with({ kind: "notice-run" }, () => "Telemetry: enabled (first run, nothing sent)") + .with({ kind: "debug" }, () => "Telemetry: debug dry-run (printing payloads, sending nothing)") + .with({ kind: "init-failed" }, () => "Telemetry: disabled (initialization failed)") + .exhaustive(); + +const formatTrackOutcome = (outcome: Exclude): string => + match(outcome) + .with({ kind: "accepted" }, (o) => `Telemetry: event accepted (${o.code})`) + .with({ kind: "rejected" }, (o) => `Telemetry: event rejected (${o.code}: ${o.message})`) + .exhaustive(); + +export const registerTelemetrySignalFlush = (telemetry: Telemetry): void => { + const signalExitCodes: ReadonlyArray = [ + ["SIGINT", 130], + ["SIGTERM", 143], + ]; + for (const [signal, exitCode] of signalExitCodes) { + process.on(signal, () => { + void telemetry.flush().finally(() => process.exit(exitCode)); + }); + } +}; + +const delay = async (ms: number): Promise => + new Promise((resolve) => { + // unref so a pending timeout never keeps the process alive after the command ends + setTimeout(resolve, ms).unref(); + }); diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..7058eb7 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,83 @@ +import chalk from "chalk"; +import type { Argv } from "yargs"; + +export type LogLevel = "none" | "standard" | "verbose"; + +const logLevelsPriority: Readonly> = { + none: 0, + standard: 10, + verbose: 20, +}; + +export const allLogLevels = Object.keys(logLevelsPriority); + +type LoggableLogLevel = Exclude; + +const defaultLogLevel: LogLevel = "standard"; + +export type LogOptions = Readonly<{ + logLevel?: string; + verbose?: boolean; +}>; + +export const addLogLevelOptions = ( + inputYargs: Argv, +): Argv => + inputYargs + .option("logLevel", { + type: "string", + choices: allLogLevels, + alias: "ll", + describe: `Set the level of details printed. (default: ${defaultLogLevel})`, + }) + .option("verbose", { + type: "boolean", + describe: "Set the log level to verbose. (alias for --logLevel=verbose)", + conflicts: "logLevel", + }); + +export const logError = (options: LogOptions, ...messages: ReadonlyArray) => + logInternal( + options, + "standard", + console.error, + ...messages.map((m) => `${chalk.red("Error:")} ${m}\n`), + ); + +export const logWarning = ( + options: LogOptions, + logAtLevel: LoggableLogLevel, + ...messages: ReadonlyArray +) => logInternal(options, logAtLevel, console.warn, ...messages); + +export const logInfo = ( + options: LogOptions, + logAtLevel: LoggableLogLevel, + ...messages: ReadonlyArray +) => logInternal(options, logAtLevel, console.log, ...messages); + +const logInternal = ( + options: LogOptions, + thisMessageLogLevel: LoggableLogLevel, + logFnc: (...msgs: ReadonlyArray) => void, + ...messages: ReadonlyArray +) => { + if (logLevelsPriority[optionsToLogLevel(options)] >= logLevelsPriority[thisMessageLogLevel]) { + logFnc(...messages); + } +}; + +export const isVerbose = (options: LogOptions): boolean => optionsToLogLevel(options) === "verbose"; + +const optionsToLogLevel = (options: LogOptions): LogLevel => { + if (options.verbose) { + return "verbose"; + } + const logLevel = options.logLevel ?? defaultLogLevel; + if (!isLogLevel(logLevel)) { + throw new Error(`CLI argument parsing error: log level "${options.logLevel}" is not valid.`); + } + return logLevel; +}; + +const isLogLevel = (input: string): input is LogLevel => allLogLevels.includes(input); diff --git a/src/managementClientFactory.ts b/src/managementClientFactory.ts deleted file mode 100644 index b6544b2..0000000 --- a/src/managementClientFactory.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ManagementClient } from '@kontent-ai/management-sdk'; -import { HttpService } from '@kontent-ai/core-sdk'; -const packageInfo = require('../package.json'); -import * as dotEnv from 'dotenv'; - -interface ICreateManagementClientParams { - readonly environmentId: string; - readonly apiKey: string; - readonly logHttpServiceErrorsToConsole: boolean; -} - -const retryAbleCodes = [429, 503]; - -dotEnv.config(); - -export const createManagementClient = (params: ICreateManagementClientParams): ManagementClient => { - const httpService = new HttpService({ - logErrorsToConsole: params.logHttpServiceErrorsToConsole, - axiosRequestConfig: { - headers: { - 'X-KC-SOURCE': `${packageInfo.name};${packageInfo.version}`, - }, - }, - }); - - return new ManagementClient({ - httpService: httpService, - environmentId: params.environmentId, - apiKey: params.apiKey, - baseUrl: process.env.BASE_URL, - retryStrategy: { - addJitter: true, - deltaBackoffMs: 1000, - maxAttempts: 10, - canRetryError: (error: any) => { - const timeout = error.errno === 'ETIMEDOUT'; - return timeout || (error.response && retryAbleCodes.includes(error.response.status)); - }, - }, - }); -}; diff --git a/src/models/environment.ts b/src/models/environment.ts deleted file mode 100644 index 4e8a521..0000000 --- a/src/models/environment.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface IEnvironment { - readonly environmentId: string; - readonly apiKey: string; -} - -export interface IEnvironmentsConfig { - [index: string]: IEnvironment; -} diff --git a/src/models/migration.ts b/src/models/migration.ts deleted file mode 100644 index 2c334c2..0000000 --- a/src/models/migration.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MigrationModule } from '../types'; - -export interface IMigration { - name: string; - module: MigrationModule; -} diff --git a/src/models/range.ts b/src/models/range.ts deleted file mode 100644 index 1778ec0..0000000 --- a/src/models/range.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IRange { - from: T; - to: T; -} diff --git a/src/models/status.ts b/src/models/status.ts deleted file mode 100644 index 26020aa..0000000 --- a/src/models/status.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface IMigrationStatus { - readonly name: string; - readonly success: boolean; - readonly order: number | Date; - readonly time: Date; - readonly lastOperation?: Operation; -} - -export interface IStatus { - [environmentId: string]: IMigrationStatus[]; -} - -export type Operation = 'run' | 'rollback'; diff --git a/src/models/templateType.ts b/src/models/templateType.ts deleted file mode 100644 index 6bf1387..0000000 --- a/src/models/templateType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum TemplateType { - TypeScript, - Javascript, -} diff --git a/src/tests/__snapshots__/createMigration.test.ts.snap b/src/tests/__snapshots__/createMigration.test.ts.snap deleted file mode 100644 index b73b15d..0000000 --- a/src/tests/__snapshots__/createMigration.test.ts.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Migration generation Creates new javascript migration with name 1`] = ` -" -const migration = { - order: 1, - run: async (apiClient) => { - }, - rollback: async(apiClient) => { - }, -}; - -module.exports = migration; -" -`; - -exports[`Migration generation Creates new typescript migration with name 1`] = ` -"import {MigrationModule} from \\"@kontent-ai/cli\\"; - -const migration: MigrationModule = { - order: 1, - run: async (apiClient) => { - }, - rollback: async(apiClient) => { - }, -}; - -export default migration; -" -`; diff --git a/src/tests/__snapshots__/statusManager.test.ts.snap b/src/tests/__snapshots__/statusManager.test.ts.snap deleted file mode 100644 index 04efc25..0000000 --- a/src/tests/__snapshots__/statusManager.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Status manager Environment has success status in status manager file 1`] = ` -Object { - "lastOperation": "run", - "name": "migration1", - "order": 1, - "success": true, - "time": "2019-12-10T01:01:00.000Z", -} -`; diff --git a/src/tests/cmds/run/getRangeDate.test.ts b/src/tests/cmds/run/getRangeDate.test.ts deleted file mode 100644 index 3a47e78..0000000 --- a/src/tests/cmds/run/getRangeDate.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { getRangeDate } from '../../../cmds/migration/run'; - -describe('test getRange', () => { - const correctRanges = [ - { - range: 'T2023:2024', - expected: { - from: new Date(Date.UTC(2023, 0)), - to: new Date(Date.UTC(2024, 0)), - }, - }, - { - range: 'T2023-03:2024', - expected: { - from: new Date(Date.UTC(2023, 2)), - to: new Date(Date.UTC(2024, 0)), - }, - }, - { - range: 'T2023-03:2024-05-23', - expected: { - from: new Date(Date.UTC(2023, 2)), - to: new Date(Date.UTC(2024, 4, 23)), - }, - }, - { - range: 'T2023-03-05-09-05-20:2024-05-23', - expected: { - from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)), - to: new Date(Date.UTC(2024, 4, 23)), - }, - }, - { - range: 'T2023-03-05-09-05-20:2023-03-05-09-05-21', - expected: { - from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)), - to: new Date(Date.UTC(2023, 2, 5, 9, 5, 21)), - }, - }, - { - range: 'T2023-03-05-09-05-20:2023-03-05-09-05-20', - expected: { - from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)), - to: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)), - }, - }, - ]; - - test.each(correctRanges)('test $range to return correct object', ({ range, expected }) => { - const result = getRangeDate(range); - - expect(result).toStrictEqual(expected); - }); - - const malformedRanges = ['T', '2023', 'T2023', 'T2023:', 'T2023:05:2024', 'T2023-13:2024', 'T2023-12:2023-12:02', 'T2023-1:2024', 'T2023-01-02-03-04-05:2023-01-02-03-04-04']; - - test.each(malformedRanges)('test %s to be null', (range) => { - const result = getRangeDate(range); - - expect(result).toBeNull(); - }); -}); diff --git a/src/tests/cmds/run/runMigration.test.ts b/src/tests/cmds/run/runMigration.test.ts deleted file mode 100644 index 6241f30..0000000 --- a/src/tests/cmds/run/runMigration.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import yargs from 'yargs'; -import { IMigration } from '../../../models/migration'; -import * as migrationUtils from '../../../utils/migrationUtils'; -import * as statusManager from '../../../utils/statusManager'; - -const { handler } = require('../../../cmds/migration/run'); - -const migrations: IMigration[] = [ - { - name: 'test1', - module: { - order: new Date('2023-03-25T10:00:00.000Z'), - run: async () => { - console.log('test1'); - }, - }, - }, - { - name: 'test2', - module: { - order: new Date('2023-03-26T10:00:00.000Z'), - run: async () => { - console.log('test2'); - }, - }, - }, - { - name: 'test3', - module: { - order: new Date('2023-03-27T10:00:00.000Z'), - run: async () => { - console.log('test3'); - }, - }, - }, -]; - -jest.spyOn(statusManager, 'markAsCompleted').mockImplementation(async () => {}); -jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(async () => ({})); -jest.spyOn(migrationUtils, 'loadMigrationFiles').mockReturnValue( - new Promise((resolve) => { - resolve(migrations); - }) -); - -const migration1 = jest.spyOn(migrations[0].module, 'run'); -const migration2 = jest.spyOn(migrations[1].module, 'run'); -const migration3 = jest.spyOn(migrations[2].module, 'run'); - -const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { - return undefined as never; -}); - -describe('run migration command tests', () => { - it('with date range (T2023-03-25:2023-03-27), two migrations should be called', async () => { - const args: any = yargs.parse([], { - apiKey: '', - environmentId: '', - range: 'T2023-03-25:2023-03-27', - }); - - await handler(args); - - expect(migration1).toBeCalled(); - expect(migration2).toBeCalled(); - expect(migration3).not.toBeCalled(); - - expect(mockExit).toHaveBeenCalledWith(0); - }); -}); diff --git a/src/tests/cmds/run/runMigrationWithRollback.test.ts b/src/tests/cmds/run/runMigrationWithRollback.test.ts deleted file mode 100644 index fd3375b..0000000 --- a/src/tests/cmds/run/runMigrationWithRollback.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import yargs from 'yargs'; -import { IMigration } from '../../../models/migration'; -import * as migrationUtils from '../../../utils/migrationUtils'; -import * as statusManager from '../../../utils/statusManager'; -import { IStatus } from '../../../models/status'; - -const { handler } = require('../../../cmds/migration/run'); - -const migrations: IMigration[] = [ - { - name: 'test1', - module: { - order: 1, - run: async () => { - console.log('test1'); - }, - rollback: async () => { - console.log('rollback 1'); - }, - }, - }, - { - name: 'test2', - module: { - order: 2, - run: async () => { - console.log('test2'); - }, - rollback: async () => { - console.log('rollback 2'); - }, - }, - }, - { - name: 'test3', - module: { - order: 3, - run: async () => { - console.log('test3'); - }, - rollback: async () => { - console.log('rollback 3'); - }, - }, - }, -]; - -const migrationStatus: IStatus = { - 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2': [ - { - name: 'test1', - success: true, - order: 1, - time: new Date(Date.now()), - lastOperation: 'run', - }, - { - name: 'test2', - success: true, - order: 2, - time: new Date(Date.now()), - lastOperation: 'rollback', - }, - { - name: 'test3', - success: true, - order: 3, - time: new Date(Date.now()), - lastOperation: 'run', - }, - ], -}; - -const migrationStatusAllRollbacks: IStatus = { - 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2': [ - { - name: 'test1', - success: true, - order: 1, - time: new Date(Date.now()), - lastOperation: 'rollback', - }, - { - name: 'test2', - success: true, - order: 2, - time: new Date(Date.now()), - lastOperation: 'rollback', - }, - { - name: 'test3', - success: true, - order: 3, - time: new Date(Date.now()), - lastOperation: 'rollback', - }, - ], -}; - -describe('run migration command tests', () => { - let mockExit: jest.SpyInstance; - beforeEach(() => { - jest.restoreAllMocks(); - jest.clearAllMocks(); - - jest.spyOn(statusManager, 'markAsCompleted').mockImplementation(async () => {}); - - jest.spyOn(migrationUtils, 'loadMigrationFiles').mockReturnValue( - new Promise((resolve) => { - resolve(migrations); - }) - ); - - mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { - return undefined as never; - }); - }); - - it('with date range all, all rollbacks should be called', async () => { - jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(async () => ({})); - - const args: any = yargs.parse([], { - apiKey: '', - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - range: '', - all: true, - rollback: 'true', - }); - - const migration1 = jest.spyOn(migrations[0].module, 'rollback'); - const migration2 = jest.spyOn(migrations[1].module, 'rollback'); - const migration3 = jest.spyOn(migrations[2].module, 'rollback'); - - await handler(args); - - const migration3Order = migration3.mock.invocationCallOrder[0]; - const migration2Order = migration2.mock.invocationCallOrder[0]; - const migration1Order = migration1.mock.invocationCallOrder[0]; - - expect(migration1).toBeCalledTimes(1); - expect(migration2).toBeCalledTimes(1); - expect(migration3).toBeCalledTimes(1); - - expect(migration3Order).toBeLessThan(migration2Order); - expect(migration2Order).toBeLessThan(migration1Order); - - expect(mockExit).toHaveBeenCalledWith(0); - }); - - it('with date range all, only rollback from first and last migrations should should be called', async () => { - jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(async () => migrationStatus); - - const args: any = yargs.parse([], { - apiKey: '', - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - range: '', - all: true, - rollback: 'true', - }); - - const migration1 = jest.spyOn(migrations[0].module, 'rollback'); - const migration2 = jest.spyOn(migrations[1].module, 'rollback'); - const migration3 = jest.spyOn(migrations[2].module, 'rollback'); - - await handler(args); - - const migration3Order = migration3.mock.invocationCallOrder[0]; - const migration1Order = migration1.mock.invocationCallOrder[0]; - - expect(migration3).toBeCalledTimes(1); - expect(migration2).not.toBeCalled(); - - expect(migration3Order).toBeLessThan(migration1Order); - expect(migration1).toBeCalledTimes(1); - - expect(mockExit).toHaveBeenCalledWith(0); - }); - - it('with date range all, only rollback from first and last migrations should should be called', async () => { - jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(async () => migrationStatusAllRollbacks); - - const args: any = yargs.parse([], { - apiKey: '', - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - range: '', - all: true, - rollback: 'true', - }); - - const migration1 = jest.spyOn(migrations[0].module, 'rollback'); - const migration2 = jest.spyOn(migrations[1].module, 'rollback'); - const migration3 = jest.spyOn(migrations[2].module, 'rollback'); - - await handler(args); - - expect(migration3).not.toBeCalled(); - expect(migration2).not.toBeCalled(); - expect(migration1).not.toBeCalled(); - - expect(mockExit).toHaveBeenCalledWith(0); - }); -}); diff --git a/src/tests/createMigration.test.ts b/src/tests/createMigration.test.ts deleted file mode 100644 index f823fee..0000000 --- a/src/tests/createMigration.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { generatePlainMigration, generateTypedMigration } from '../utils/migrationUtils'; - -describe('Migration generation', () => { - it('Creates new typescript migration with name', () => { - const migrationContent = generateTypedMigration(); - expect(migrationContent).toMatchSnapshot(); - }); - - it('Creates new javascript migration with name', () => { - const migrationContent = generatePlainMigration(); - - expect(migrationContent).toMatchSnapshot(); - }); -}); diff --git a/src/tests/duplicateMigration.test.ts b/src/tests/duplicateMigration.test.ts deleted file mode 100644 index 812b21a..0000000 --- a/src/tests/duplicateMigration.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getDuplicates } from '../utils/migrationUtils'; - -describe('Detection of duplicate orders', () => { - it('Finds duplicates migration', () => { - const migrations = [ - { - module: { - order: 1, - name: 'test', - }, - }, - { - module: { - order: 2, - name: 'test', - }, - }, - { - module: { - order: 1, - name: 'test', - }, - }, - ]; - - const result = getDuplicates(migrations, (opt) => opt.module.order); - - expect(result.length).toBe(2); - }); - - it('No duplicate migration is found', () => { - const migrations = [ - { - module: { - order: 1, - name: 'test', - }, - }, - { - module: { - order: 2, - name: 'test', - }, - }, - { - module: { - order: 3, - name: 'test', - }, - }, - ]; - - const result = getDuplicates(migrations, (opt) => opt.module.order); - - expect(result.length).toBe(0); - }); -}); diff --git a/src/tests/fileUtils.test.ts b/src/tests/fileUtils.test.ts deleted file mode 100644 index 27b07dd..0000000 --- a/src/tests/fileUtils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getFileWithExtension, isAllowedExtension } from '../utils/fileUtils'; - -describe('File utils', () => { - it('Returns file with extension when input has valid file extension', () => { - const input = 'testFile.js'; - const result = getFileWithExtension(input); - expect(result).toBe('testFile.js'); - }); - - it('Returns file with extension even input has no file extension', () => { - const input = 'testFile'; - const result = getFileWithExtension(input); - expect(result).toBe('testFile.js'); - }); - - it('File without extension is allowed', () => { - const input = 'testFile'; - const result = isAllowedExtension(input); - expect(result).toBe(true); - }); - - it('File with .JS extension is allowed', () => { - const input = 'testFile.js'; - const result = isAllowedExtension(input); - expect(result).toBe(true); - }); - - it('File with .TS extension is not allowed', () => { - const input = 'testFile.ts'; - const result = isAllowedExtension(input); - expect(result).toBe(false); - }); -}); diff --git a/src/tests/invalidOrderMigration.test.ts b/src/tests/invalidOrderMigration.test.ts deleted file mode 100644 index ca72048..0000000 --- a/src/tests/invalidOrderMigration.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getMigrationsWithInvalidOrder } from '../utils/migrationUtils'; - -describe('Detection of invalid orders', () => { - it('Finds migrations with invalid order', () => { - const migrations = [ - { - module: { - order: -1, - }, - name: 'test', - }, - { - module: { - order: 1.1, - }, - name: 'test', - }, - { - module: { - order: 2, - }, - name: 'test', - }, - { - module: { - order: 'aaa', - }, - name: 'test', - }, - ]; - - const result = getMigrationsWithInvalidOrder(migrations); - - expect(result.length).toBe(3); - expect(result[0].module.order).toBe(-1); - expect(result[1].module.order).toBe(1.1); - expect(result[2].module.order).toBe('aaa'); - }); - - it('No invalid order is found', () => { - const migrations = [ - { - module: { - order: 1, - name: 'test', - }, - }, - { - module: { - order: 2, - name: 'test', - }, - }, - { - module: { - order: 6, - name: 'test', - }, - }, - { - module: { - order: 7, - name: 'test', - }, - }, - ]; - - const result = getMigrationsWithInvalidOrder(migrations); - - expect(result.length).toBe(0); - }); -}); diff --git a/src/tests/runMigration.test.ts b/src/tests/runMigration.test.ts deleted file mode 100644 index 16ce9c1..0000000 --- a/src/tests/runMigration.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { IMigration } from '../models/migration'; -import { runMigration } from '../utils/migrationUtils'; -import * as statusManager from '../utils/statusManager'; -import { createManagementClient } from '@kontent-ai/management-sdk'; - -jest.spyOn(statusManager, 'markAsCompleted').mockImplementation(async () => {}); - -describe('statusManager runMigration function', () => { - it('runMigration no rollback function specified should throw', async () => { - const migration: IMigration = { - name: 'test_migration', - module: { - order: 1, - run: async () => {}, - }, - }; - - const returnCode = await runMigration({}, migration, { - client: createManagementClient({ apiKey: '' }), - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - operation: 'rollback', - saveStatusFromPlugin: null, - }); - - expect(returnCode).toEqual(1); - }); - - it('runMigration rollback function specified should call rollback', async () => { - const migration: IMigration = { - name: 'test_migration', - module: { - order: 1, - run: async () => {}, - rollback: async () => {}, - }, - }; - - const rollback = jest.spyOn(migration.module, 'rollback'); - - const returnCode = await runMigration({}, migration, { - client: createManagementClient({ apiKey: '' }), - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - operation: 'rollback', - saveStatusFromPlugin: null, - }); - - expect(rollback).toBeCalledTimes(1); - expect(returnCode).toEqual(0); - }); - - it('runMigration should call run', async () => { - const migration: IMigration = { - name: 'test_migration', - module: { - order: 1, - run: async () => {}, - rollback: async () => {}, - }, - }; - - const run = jest.spyOn(migration.module, 'run'); - - const returnCode = await runMigration({}, migration, { - client: createManagementClient({ apiKey: '' }), - environmentId: 'fcb801c6-fe1d-41cf-af91-ec13802a1ed2', - operation: 'run', - saveStatusFromPlugin: null, - }); - - expect(run).toBeCalledTimes(1); - expect(returnCode).toEqual(0); - }); -}); diff --git a/src/tests/statusManager.test.ts b/src/tests/statusManager.test.ts deleted file mode 100644 index 4395d83..0000000 --- a/src/tests/statusManager.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as statusManager from '../utils/statusManager'; -import * as fileUtils from '../utils/fileUtils'; -import * as fs from 'fs'; -import * as path from 'path'; -import { IStatus } from '../models/status'; - -const readStatusFile = (): IStatus => { - const statusFilepath = path.join(process.cwd(), 'status.json'); - - const fileContent = fs.readFileSync(statusFilepath).toString(); - return JSON.parse(fileContent); -}; - -describe('Status manager', () => { - beforeAll(() => { - jest.spyOn(Date, 'now').mockImplementation(() => 1575939660000); - }); - - it('Environment has success status in status manager file', async () => { - const environmentId = 'environment1'; - const migrationName = 'migration1'; - await statusManager.markAsCompleted({}, environmentId, migrationName, 1, 'run', null); - - const statusFile = readStatusFile(); - const status = statusFile[environmentId][0]; - - expect(status).toMatchSnapshot(); - }); - - it('Not executed migration is not present in status file', async () => { - const environment1 = 'environment1'; - const environment2 = 'environment2'; - const migration1Name = 'migration1'; - await statusManager.markAsCompleted({}, environment1, migration1Name, 1, 'run', null); - - const environmentMigrationStatus = statusManager.shouldSkipMigration({}, migration1Name, environment2, 'run'); - - expect(environmentMigrationStatus).toBe(false); - }); - - it('Executed migration is present in status file', async () => { - const environmentId2 = 'environment2'; - const migration2Name = 'migration2'; - await statusManager.markAsCompleted({}, environmentId2, migration2Name, 1, 'run', null); - - const environmentMigrationStatus = statusManager.shouldSkipMigration({}, environmentId2, migration2Name, 'run'); - - expect(environmentMigrationStatus).toBe(false); - }); - - it('loadMigrationsExecutionStatus to be called with plugins', async () => { - const expectedStatus = { '30816c62-8d41-4dc4-a1ab-40440070b7bf': [] }; - - const readStatusPlugin = async () => { - return expectedStatus; - }; - - const returnedStatus = await statusManager.loadMigrationsExecutionStatus(readStatusPlugin); - - expect(returnedStatus).toEqual(expectedStatus); - }); - - it('loadMigrationsExecutionStatus to be called with plugin that throw', async () => { - const readStatusPlugin = async () => { - throw new Error('Error during plugin function'); - }; - - expect.assertions(1); - - return statusManager.loadMigrationsExecutionStatus(readStatusPlugin).catch((e) => expect((e as Error).message).toMatch('Error during plugin function')); - }); - - it('MarkAsCompleted to be called with plugins', async () => { - jest.spyOn(fileUtils, 'fileExists').mockReturnValue(true); - - const saveStatusMocked = jest.fn().mockImplementation(() => Promise.resolve()); - - await statusManager.markAsCompleted({}, '', 'testMigration', 1, 'run', saveStatusMocked); - - expect(saveStatusMocked).toHaveBeenCalled(); - }); -}); diff --git a/src/tests/statusPlugin/statusPlugin.test.ts b/src/tests/statusPlugin/statusPlugin.test.ts deleted file mode 100644 index c7a3fd7..0000000 --- a/src/tests/statusPlugin/statusPlugin.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { loadStatusPlugin } from '../../utils/status/statusPlugin'; - -describe('status plugin tests', () => { - const saveStatus = () => {}; - const readStatus = () => ({}); - - jest.mock( - 'plugin', - () => ({ - saveStatus, - readStatus, - }), - { virtual: true } - ); - - jest.mock( - 'malformedPlugin', - () => ({ - save: saveStatus, - read: readStatus, - }), - { virtual: true } - ); - - it('test correct plugin', async () => { - const functions = await loadStatusPlugin('plugin'); - - expect(functions.saveStatus).toEqual(saveStatus); - expect(functions.readStatus).toEqual(readStatus); - }); - - it('test malformed plugin', async () => { - expect(loadStatusPlugin('malformedPlugin')).rejects.toThrow(); - }); -}); diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 66d0d68..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ManagementClient } from '@kontent-ai/management-sdk'; -import { IStatus } from '../models/status'; - -export declare interface MigrationModule { - readonly order: number | Date; - run(apiClient: ManagementClient): Promise; - rollback?(apiClient: ManagementClient): Promise; -} - -export type SaveStatusType = (data: string) => Promise; -export type ReadStatusType = () => Promise; diff --git a/src/types/yargs.ts b/src/types/yargs.ts new file mode 100644 index 0000000..1a0bd41 --- /dev/null +++ b/src/types/yargs.ts @@ -0,0 +1,23 @@ +import type { CommandModule } from "yargs"; +import type { Telemetry } from "../lib/telemetry/tracking.js"; +import type { LogOptions } from "../log.js"; + +type MakeRequired = Omit & Required>; + +export type StandaloneCommandModule = MakeRequired< + CommandModule, + "command" | "describe" +>; + +export type CommandDeps = Readonly<{ + telemetry: Telemetry; +}>; + +export type RegisterCommand = ( + obj: Readonly<{ + command: ( + cmdModule: StandaloneCommandModule, + ) => Result; + }>, + deps: CommandDeps, +) => Result; diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts deleted file mode 100644 index 89b0b7b..0000000 --- a/src/utils/dateUtils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const formatDateForFileName = (date: Date) => - `${date.getUTCFullYear()}-` + - `${('0' + (date.getUTCMonth() + 1)).slice(-2)}-` + - `${('0' + date.getUTCDate()).slice(-2)}-` + - `${('0' + date.getUTCHours()).slice(-2)}-` + - `${('0' + date.getUTCMinutes()).slice(-2)}-` + - `${('0' + date.getUTCSeconds()).slice(-2)}-`; diff --git a/src/utils/environmentUtils.ts b/src/utils/environmentUtils.ts deleted file mode 100644 index 62d276f..0000000 --- a/src/utils/environmentUtils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import chalk from 'chalk'; -import fs from 'fs'; -import * as path from 'path'; -import { IEnvironmentsConfig } from '../models/environment'; -import { fileExists } from './fileUtils'; - -const environmentsConfigName = '.environments.json'; - -export const saveEnvironmentConfig = (name: string, environmentId: string, apiKey: string) => { - const environments = getEnvironmentsConfig(); - if (environments[name]) { - console.log(chalk.yellow(`The \"${name}\" environment already exists and will be overwritten.`)); - } - - environments[name] = { environmentId, apiKey }; - const environmentsJSON = JSON.stringify(environments, null, 2); - - saveEnvironmentData(environmentsJSON, name); -}; - -const saveEnvironmentData = (data: string, name: string): void => { - const configsFilepath = getEnvironmentConfigFilepath(); - - fs.writeFile(configsFilepath, data, { flag: 'w' }, (error) => { - if (error) { - console.error(chalk.red(error.stack || error.message)); - } else { - console.log(chalk.green(`The environment ${name} (\"${configsFilepath}\") was updated.`)); - } - }); -}; - -const getEnvironmentConfigFilepath = (): string => path.join(process.cwd(), environmentsConfigName); - -export const getEnvironmentsConfig = (): IEnvironmentsConfig => { - const environmentConfigFilepath = getEnvironmentConfigFilepath(); - - if (!fileExists(environmentConfigFilepath)) { - return {}; - } - - const environmentsBuffer = fs.readFileSync(environmentConfigFilepath); - - return JSON.parse(environmentsBuffer.toString()); -}; - -export const environmentConfigExists = () => { - const environmentConfigFilepath = getEnvironmentConfigFilepath(); - - return fileExists(environmentConfigFilepath); -}; diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts deleted file mode 100644 index 867374a..0000000 --- a/src/utils/fileUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import fs, { PathLike } from 'fs'; -import * as path from 'path'; - -export const fileExists = (filePath: PathLike): boolean => { - return fs.existsSync(filePath); -}; - -export const isAllowedExtension = (filename: string): boolean => { - const extension = path.extname(filename); - return ['.js', ''].includes(extension); -}; - -export const getFileWithExtension = (filename: string, defaultExtension: string = '.js'): string => { - const hasFileExtension = path.extname(filename); - - if (hasFileExtension) { - const normalized = filename.split(defaultExtension).slice(0, -1).join(defaultExtension); - return normalized + defaultExtension; - } else { - return filename + defaultExtension; - } -}; - -export const getFileBackupName = () => { - const currentDate = new Date(); - const formatted = `${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}-${currentDate.getTime()}`; - return `backup-${formatted}`; -}; diff --git a/src/utils/migrationUtils.ts b/src/utils/migrationUtils.ts deleted file mode 100644 index c4fc2ac..0000000 --- a/src/utils/migrationUtils.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { ManagementClient, SharedModels } from '@kontent-ai/management-sdk'; -import chalk from 'chalk'; -import path from 'path'; -import fs, { Dirent } from 'fs'; -import { TemplateType } from '../models/templateType'; -import { MigrationModule } from '../types'; -import { IMigration } from '../models/migration'; -import { markAsCompleted, shouldSkipMigration } from './statusManager'; -import { formatDateForFileName } from './dateUtils'; -import { StatusPlugin } from './status/statusPlugin'; -import { IStatus, Operation } from '../models/status'; - -const listMigrationFiles = (fileExtension: string): Dirent[] => { - return fs - .readdirSync(getMigrationDirectory(), { withFileTypes: true }) - .filter((f) => f.isFile()) - .filter((f) => f.name.endsWith(fileExtension)); -}; - -export const getMigrationDirectory = (): string => { - const migrationDirectory = 'Migrations'; - return path.join(process.cwd(), migrationDirectory); -}; - -export const getMigrationFilepath = (filename: string): string => { - return path.join(getMigrationDirectory(), filename); -}; - -const ensureMigrationsDirectoryExists = () => { - const directory = getMigrationDirectory(); - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory); - } -}; - -export const saveMigrationFile = (migrationName: string, migrationData: string, templateType: TemplateType, orderDate: Date | null): string => { - ensureMigrationsDirectoryExists(); - const fileExtension = templateType === TemplateType.TypeScript ? '.ts' : '.js'; - const date = orderDate ? `${formatDateForFileName(orderDate)}` : ''; - const migrationFilepath = getMigrationFilepath(date + migrationName + fileExtension); - - try { - fs.writeFileSync(migrationFilepath, migrationData); - console.log(chalk.green(`Migration template ${migrationName} (${migrationFilepath}) was generated.`)); - } catch (e) { - console.error("Couldn't save the migration.", e instanceof Error ? e.message : 'Unknown error occurred.'); - } - - return migrationFilepath; -}; - -interface RunMigrationOptions { - client: ManagementClient; - environmentId: string; - operation: Operation; - saveStatusFromPlugin: StatusPlugin['saveStatus'] | null; -} - -export const runMigration = async (migrationsStatus: IStatus, migration: IMigration, options: RunMigrationOptions): Promise => { - const { client, environmentId, operation, saveStatusFromPlugin } = options; - - console.log(`Running the ${operation === 'rollback' ? 'rollback of' : ''} ${migration.name} migration.`); - - let isSuccess = true; - - try { - if (operation === 'run') { - await migration.module.run(client); - await markAsCompleted(migrationsStatus, environmentId, migration.name, migration.module.order, operation, saveStatusFromPlugin); - } else { - if (!migration.module.rollback) { - throw new Error('No rollback function specified'); - } - - await migration.module.rollback(client); - await markAsCompleted(migrationsStatus, environmentId, migration.name, migration.module.order, operation, saveStatusFromPlugin); - } - } catch (e) { - console.error(chalk.redBright('An error occurred while running migration:'), chalk.yellowBright(migration.name), chalk.redBright('see the output from running the script.')); - - let error = e as any; - if (e instanceof SharedModels.ContentManagementBaseKontentError && e.originalError !== undefined) { - console.group('Error details'); - console.error(chalk.redBright('Message:'), e.message); - console.error(chalk.redBright('Code:'), e.errorCode); - console.error(chalk.redBright('Validation Errors:'), e.validationErrors); - console.groupEnd(); - console.log(); - console.group('Response details:'); - console.error(chalk.redBright('Message:'), e.originalError.message); - console.groupEnd(); - - error = e.originalError; - } else { - console.group('Error details'); - console.error(chalk.redBright('Message:'), e instanceof Error ? e.message : 'Unknown error'); - console.groupEnd(); - } - - const requestConfig = error.config; - if (requestConfig) { - const bodyData = JSON.parse(requestConfig.data || {}); - console.log(); - console.group('Request details:'); - console.error(chalk.yellow('URL:'), requestConfig.url); - console.error(chalk.yellow('Method:'), requestConfig.method); - console.error(chalk.yellow('Body:'), bodyData); - console.groupEnd(); - } - - isSuccess = false; - } - - if (!isSuccess) { - return 1; - } - - console.log(chalk.green(`The ${operation === 'rollback' ? 'rollback of ' : ''}\"${migration.name}\" migration on an environment with ID \"${environmentId}\" executed successfully.`)); - return 0; -}; - -export const generateTypedMigration = (orderDate?: Date | null): string => { - const order = orderDate ? `new Date('${orderDate.toISOString()}')` : '1'; - return `import {MigrationModule} from "@kontent-ai/cli"; - -const migration: MigrationModule = { - order: ${order}, - run: async (apiClient) => { - }, - rollback: async(apiClient) => { - }, -}; - -export default migration; -`; -}; - -export const generatePlainMigration = (orderDate?: Date | null): string => { - const order = orderDate ? `new Date('${orderDate.toISOString()}')` : '1'; - - return ` -const migration = { - order: ${order}, - run: async (apiClient) => { - }, - rollback: async(apiClient) => { - }, -}; - -module.exports = migration; -`; -}; - -export const createMigration = (migrationName: string, templateType: TemplateType, useTimestampOrder: boolean): string => { - ensureMigrationsDirectoryExists(); - const orderDate = true === useTimestampOrder ? new Date() : null; - orderDate?.setMilliseconds(0); - const generatedMigration = templateType === TemplateType.TypeScript ? generateTypedMigration(orderDate) : generatePlainMigration(orderDate); - - return saveMigrationFile(migrationName, generatedMigration, templateType, orderDate); -}; - -export const getDuplicates = (array: T[], key: (obj: T) => number | Date): T[] => { - const allEntries = new Map(); - let duplicates: T[] = []; - - for (const item of array) { - const itemKey = key(item); - const prevItem = allEntries.get(itemKey) || []; - allEntries.set(itemKey, prevItem.concat(item)); - } - - for (const [, value] of allEntries.entries()) { - if (value.length > 1) { - duplicates = duplicates.concat(value); - } - } - - return duplicates; -}; - -export const getMigrationsWithInvalidOrder = (array: T[]): T[] => { - const migrationsWithInvalidOrder: T[] = []; - - for (const migration of array) { - if (migration.module.order instanceof Date) { - continue; - } - - if (!Number.isInteger(migration.module.order) || Number(migration.module.order) < 0) { - migrationsWithInvalidOrder.push(migration); - } - } - - return migrationsWithInvalidOrder; -}; - -export const loadModule = async (migrationFile: string): Promise => { - const migrationPath = getMigrationFilepath(migrationFile); - - return await import(migrationPath) - .then(async (module) => { - const importedModule: MigrationModule = module.default; - return importedModule; - }) - .catch((error) => { - throw new Error(chalk.red(`Couldn't import the migration script from \"${migrationPath}"\ due to an error: \"${error.message}\".`)); - }); -}; - -export const loadMigrationFiles = async (): Promise => { - const migrations: IMigration[] = []; - - const files = listMigrationFiles('.js'); - - for (const file of files) { - migrations.push({ name: file.name, module: await loadModule(file.name) }); - } - - return migrations.filter(String); -}; - -export const getSuccessfullyExecutedMigrations = (migrationsStatus: IStatus, migrations: IMigration[], environmentId: string, operation: Operation): IMigration[] => { - const alreadyExecutedMigrations: IMigration[] = []; - - // filter by execution status - for (const migration of migrations) { - if (shouldSkipMigration(migrationsStatus, migration.name, environmentId, operation)) { - alreadyExecutedMigrations.push(migration); - } - } - - return alreadyExecutedMigrations.filter(String); -}; diff --git a/src/utils/status/statusPlugin.ts b/src/utils/status/statusPlugin.ts deleted file mode 100644 index 05cb148..0000000 --- a/src/utils/status/statusPlugin.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ReadStatusType, SaveStatusType } from '../../types'; - -export interface StatusPlugin { - saveStatus: SaveStatusType; - readStatus: ReadStatusType; -} - -export const loadStatusPlugin = async (path: string): Promise => { - const pluginModule = await import(path); - - if (!('saveStatus' in pluginModule && typeof pluginModule.saveStatus === 'function') || !('readStatus' in pluginModule && typeof pluginModule.readStatus === 'function')) { - throw new Error('Invalid plugin: does not implement saveStatus or readStatus functions'); - } - - return { - saveStatus: pluginModule.saveStatus, - readStatus: pluginModule.readStatus, - }; -}; diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts deleted file mode 100644 index d31bee6..0000000 --- a/src/utils/statusManager.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { IMigrationStatus, IStatus, Operation } from '../models/status'; -import { readFileSync, writeFileSync } from 'fs'; -import { fileExists } from './fileUtils'; -import * as path from 'path'; -import { type StatusPlugin } from './status/statusPlugin'; - -const migrationStatusFilename = 'status.json'; -const pluginsFilename = 'plugins.js'; - -const updateMigrationStatus = async (status: IStatus, environmentId: string, migrationStatus: IMigrationStatus, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { - status[environmentId] = status[environmentId] === undefined ? [] : status[environmentId]; - const environmentsMigrationsHistory = status[environmentId]; - - const previousMigrationStatusIndex = environmentsMigrationsHistory.findIndex((executedMigration) => executedMigration.name === migrationStatus.name); - if (previousMigrationStatusIndex > -1) { - environmentsMigrationsHistory[previousMigrationStatusIndex] = migrationStatus; - } else { - environmentsMigrationsHistory.push(migrationStatus); - } - - await saveStatusFile(status, saveStatusFromPlugin); -}; - -export const markAsCompleted = async (status: IStatus, environmentId: string, name: string, order: number | Date, operation: Operation, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { - const migrationStatus: IMigrationStatus = { - name, - order, - success: true, - time: new Date(Date.now()), - lastOperation: operation, - }; - - await updateMigrationStatus(status, environmentId, migrationStatus, saveStatusFromPlugin); -}; - -const saveStatusFile = async (migrationsStatus: IStatus, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { - const statusJSON = JSON.stringify(migrationsStatus, null, 2); - - if (saveStatusFromPlugin) { - try { - await saveStatusFromPlugin(statusJSON); - return; - } catch (e) { - console.error(`The error ${e} occured when using saveStatus function from plugin. Fallbacking to write status into status.json`); - saveStatusToFile(statusJSON); - throw new Error((e as Error).message); - } - } - - saveStatusToFile(statusJSON); -}; - -const getMigrationStatus = (migrationsStatus: IStatus, migrationName: string, environmentId: string): IMigrationStatus | null => { - const environmentStatus = migrationsStatus[environmentId]; - - return environmentStatus === undefined ? null : environmentStatus.find((migrationStatus) => migrationStatus.name === migrationName) ?? null; -}; - -export const shouldSkipMigration = (migrationsStatus: IStatus, migrationName: string, environmentId: string, operation: Operation): boolean => { - const migrationStatus = getMigrationStatus(migrationsStatus, migrationName, environmentId); - - if (migrationStatus === null || !migrationStatus.success) { - return false; - } - - return (migrationStatus?.lastOperation ?? 'run') === operation; -}; - -const getStatusFilepath = (): string => { - return path.join(process.cwd(), migrationStatusFilename); -}; - -export const loadMigrationsExecutionStatus = async (readStatusFromPlugin: StatusPlugin['readStatus'] | null): Promise => { - if (readStatusFromPlugin) { - return await readStatusFromPlugin(); - } - - return readFromStatus(); -}; - -const readFromStatus = (): IStatus => { - const statusFilepath = getStatusFilepath(); - if (!fileExists(statusFilepath)) { - return {}; - } - - try { - const environmentsMigrationStatuses = readFileSync(getStatusFilepath()).toString(); - - return JSON.parse(environmentsMigrationStatuses); - } catch (error) { - console.warn(`Status JSON file is invalid because of ${error instanceof Error ? error.message : 'unknown error.'}. Continuing with empty status.`); - return {}; - } -}; - -const saveStatusToFile = (data: string): void => { - const statusFilepath = getStatusFilepath(); - - try { - writeFileSync(statusFilepath, data, { flag: 'w' }); - console.log(`Status file was updated see ${statusFilepath}`); - } catch (error) { - console.error(`Status file save failed, because of ${error instanceof Error ? error.message : 'unknown error.'}`); - } -}; - -export const getPluginsFilePath = () => path.join(process.cwd(), pluginsFilename); diff --git a/test/helpers/iapiTestClient.ts b/test/helpers/iapiTestClient.ts new file mode 100644 index 0000000..0ecd810 --- /dev/null +++ b/test/helpers/iapiTestClient.ts @@ -0,0 +1,39 @@ +import { getDefaultHttpService, type HttpAdapter, type JsonValue } from "@kontent-ai/core-sdk"; +import type { IapiClient } from "../../src/lib/iapi/client.js"; + +const testBaseUrl = { protocol: "https", host: "iapi.test" } as const; +const testSdkInfo = { name: "kontent-cli-test", version: "0.0.0", host: "npmjs.com" } as const; + +export type IapiRoute = Readonly<{ + method: string; + path: RegExp; + reply: JsonValue; +}>; + +// A real iapi client whose transport answers from a declarative route table. The fake +// plugs into core-sdk's HttpAdapter seam, so the genuine endpoint/query code runs against +// it. Reusable by any command's tests, not just bootstrap. +export const iapiTestClient = (routes: readonly IapiRoute[]): IapiClient => { + const adapter: HttpAdapter = { + executeRequest: ({ url, method }) => { + const route = routes.find((r) => r.method === method && r.path.test(url.pathname)); + if (route === undefined) { + throw new Error(`No iapi stub for ${method} ${url.pathname}`); + } + return Promise.resolve({ + payload: route.reply, + responseHeaders: [], + status: 200, + statusText: "OK", + url, + }); + }, + }; + + return { + config: { baseUrl: testBaseUrl, httpService: getDefaultHttpService({ adapter }) }, + sdkInfo: testSdkInfo, + urlBase: testBaseUrl, + token: "test-token", + }; +}; diff --git a/test/integration/bootstrap.test.ts b/test/integration/bootstrap.test.ts new file mode 100644 index 0000000..0484cd2 --- /dev/null +++ b/test/integration/bootstrap.test.ts @@ -0,0 +1,169 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { confirm, isCancel, select } from "@clack/prompts"; +import { downloadTemplate } from "giget"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { performBootstrap } from "../../src/core/project/bootstrap.js"; +import { createMapiClient } from "../../src/lib/mapi/client.js"; +import { type IapiRoute, iapiTestClient } from "../helpers/iapiTestClient.js"; + +vi.mock("@clack/prompts", () => ({ + spinner: () => ({ + start: () => {}, + stop: () => {}, + error: () => {}, + message: () => {}, + }), + confirm: vi.fn(), + select: vi.fn(), + note: vi.fn(), + isCancel: vi.fn(() => false), +})); + +vi.mock("giget", () => ({ + downloadTemplate: vi.fn(), +})); + +const ENV_ID = "11111111-2222-3333-4444-555555555555"; + +const TEMPLATE = [ + "# Kickstart sample env", + "VITE_ENVIRONMENT_ID=", + "VITE_DELIVERY_API_KEY=", + "VITE_OTHER=keep-me", + "", +].join("\n"); + +const projectInfoRoute: IapiRoute = { + method: "GET", + path: /\/project-management\//, + reply: { projectName: "My Project", projectContainerId: "container-1", subscriptionId: "sub-1" }, +}; + +const kickstartPropertiesRoute: IapiRoute = { + method: "GET", + path: /\/property$/, + reply: [{ key: "SampleProjectType", value: "Kickstart" }], +}; + +const listKeysRoute = (reply: IapiRoute["reply"]): IapiRoute => ({ + method: "POST", + path: /\/keys\/listing$/, + reply, +}); + +// mapi is unused by the Kickstart sample (no preview space); a real client is fine since +// construction does no I/O and nothing here invokes it. +const mapiClient = createMapiClient({ token: "test-token", envId: ENV_ID }); + +let targetDir = ""; + +const makeParams = () => ({ logLevel: "none", envId: ENV_ID, path: targetDir }) as const; + +const readEnvLocal = () => readFile(path.join(targetDir, ".env.local"), "utf8"); + +beforeEach(async () => { + targetDir = await mkdtemp(path.join(tmpdir(), "bootstrap-test-")); + vi.clearAllMocks(); + vi.mocked(isCancel).mockReturnValue(false); + vi.mocked(downloadTemplate).mockImplementation(async (_repo, opts) => { + await writeFile(path.join((opts as { dir: string }).dir, ".env.template"), TEMPLATE, "utf8"); + return {} as Awaited>; + }); +}); + +afterEach(async () => { + await rm(targetDir, { recursive: true, force: true }); +}); + +describe("performBootstrap", () => { + it("wires .env.local using an existing delivery key the user selects", async () => { + const iapiClient = iapiTestClient([ + projectInfoRoute, + kickstartPropertiesRoute, + listKeysRoute([{ token_seed_id: "seed-123", name: "My Key", expires_at: "2030-01-01" }]), + { method: "GET", path: /\/keys\/[^/]+$/, reply: { api_key: "delivery-secret-abc" } }, + ]); + vi.mocked(select).mockResolvedValue("seed-123"); + + const result = await performBootstrap(makeParams(), { iapiClient, mapiClient }); + + expect(result.kind).toBe("ok"); + if (result.kind !== "ok") { + return; + } + expect(result.value).toEqual({ subscriptionId: "sub-1", sampleProjectType: "Kickstart" }); + + const env = await readEnvLocal(); + expect(env).toContain(`VITE_ENVIRONMENT_ID=${ENV_ID}`); + expect(env).toContain("VITE_DELIVERY_API_KEY=delivery-secret-abc"); + expect(env).toContain("VITE_OTHER=keep-me"); + }); + + it("creates a new delivery key when none exist and the user confirms", async () => { + const iapiClient = iapiTestClient([ + projectInfoRoute, + kickstartPropertiesRoute, + listKeysRoute([]), + { method: "POST", path: /\/keys$/, reply: { api_key: "new-secret-xyz", name: "new key" } }, + ]); + vi.mocked(confirm).mockResolvedValue(true); + + const result = await performBootstrap(makeParams(), { iapiClient, mapiClient }); + + expect(result.kind).toBe("ok"); + const env = await readEnvLocal(); + expect(env).toContain("VITE_DELIVERY_API_KEY=new-secret-xyz"); + }); + + it("aborts when the user cancels the create-key prompt", async () => { + const iapiClient = iapiTestClient([ + projectInfoRoute, + kickstartPropertiesRoute, + listKeysRoute([]), + ]); + vi.mocked(confirm).mockResolvedValue(true); + vi.mocked(isCancel).mockReturnValue(true); + + const result = await performBootstrap(makeParams(), { iapiClient, mapiClient }); + + expect(result.kind).toBe("err"); + if (result.kind !== "err") { + return; + } + expect(result.error.kind).toBe("aborted"); + expect(downloadTemplate).not.toHaveBeenCalled(); + }); + + it("rejects an environment whose SampleProjectType is unsupported", async () => { + const iapiClient = iapiTestClient([ + projectInfoRoute, + { method: "GET", path: /\/property$/, reply: [{ key: "SampleProjectType", value: "Nope" }] }, + ]); + + const result = await performBootstrap(makeParams(), { iapiClient, mapiClient }); + + expect(result.kind).toBe("err"); + if (result.kind !== "err") { + return; + } + expect(result.error.kind).toBe("unsupported-sample"); + expect(downloadTemplate).not.toHaveBeenCalled(); + }); + + it("fails fast when the target directory is not empty, before any API call", async () => { + await writeFile(path.join(targetDir, "existing.txt"), "occupied", "utf8"); + // Empty route table: any iapi request would throw, proving none is made. + const iapiClient = iapiTestClient([]); + + const result = await performBootstrap(makeParams(), { iapiClient, mapiClient }); + + expect(result.kind).toBe("err"); + if (result.kind !== "err") { + return; + } + expect(result.error.kind).toBe("target-not-usable"); + expect(downloadTemplate).not.toHaveBeenCalled(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 087664d..7cf6b97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,39 +1,16 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "lib": [ - "esnext", - "DOM" - ], - "sourceMap": true, - "allowJs": true, - "outDir": "./lib/", - "strict": true, - "declaration": true, - "rootDir": "src", - "moduleResolution": "node", - "alwaysStrict": true, - "strictPropertyInitialization": true, - "forceConsistentCasingInFileNames": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "strictFunctionTypes": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "declarationDir": "./lib", - "declarationMap": true, - "typeRoots": [ - "./node_modules/@types" - ], - "esModuleInterop": true, - "resolveJsonModule": true - }, - "exclude": [ - "coverage", - "node_modules", - "Migrations", - "lib", - "./*.js" - ] -} \ No newline at end of file +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*", "test/**/*", "vitest.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..689d818 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsdown"; + +// Pick up build-time variables (e.g. KONTENT_CLI_AMPLITUDE_API_KEY) from a +// local .env file; real environment variables take precedence. +try { + process.loadEnvFile(); +} catch { + // no .env file present; fine +} + +export default defineConfig({ + entry: ["src/index.ts"], + define: { + __AMPLITUDE_API_KEY__: JSON.stringify(process.env.KONTENT_CLI_AMPLITUDE_API_KEY ?? ""), + __KONTENT_URL__: JSON.stringify(process.env.KONTENT_URL ?? ""), + __AUTH0_DOMAIN__: JSON.stringify(process.env.KONTENT_AUTH0_DOMAIN ?? ""), + __AUTH0_CLIENT_ID__: JSON.stringify(process.env.KONTENT_AUTH0_CLIENT_ID ?? ""), + __AUTH0_AUDIENCE__: JSON.stringify(process.env.KONTENT_AUTH0_AUDIENCE ?? ""), + }, + format: "esm", + target: "node22", + platform: "node", + outDir: "dist", + clean: true, + shims: false, + dts: false, + sourcemap: true, + unbundle: false, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ed8bf77 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + }, +});