diff --git a/.eslintrc b/.eslintrc index fc09a8f..adb1e42 100644 --- a/.eslintrc +++ b/.eslintrc @@ -62,8 +62,7 @@ { "devDependencies": true, "optionalDependencies": false, - "peerDependencies": false, - "packageDir": "./" + "peerDependencies": false } ], "@typescript-eslint/consistent-type-imports": [ @@ -72,6 +71,13 @@ "prefer": "type-imports", "disallowTypeAnnotations": false } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } ] }, "env": { diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2ebdc1a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + # The default branch is next. Omit target-branch so security groups apply too. + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + commit-message: + prefix: "fix(*)" + groups: + security: + applies-to: security-updates + patterns: ["*"] + development: + dependency-type: development + update-types: [minor, patch] + versioning-strategy: increase + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: ci + groups: + actions: + patterns: ["*"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..dbc2b5e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Change + +Describe the problem and resulting behavior. Target `next` and use a Conventional Commit title, for example `fix(cli): handle missing snapshots`. + +## Validation + +List the relevant checks and their results. + +## Release impact + +Describe any compatibility change. Tagging or publishing requires Omer's explicit approval; a PR is not release approval. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e8be7a..26e8413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,126 +1,43 @@ name: Continuous Integration -on: [pull_request] - +on: + pull_request: + branches: [next] + push: + branches: [next] permissions: - id-token: write contents: read - checks: write - +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - build: - name: Build + verify: + name: Verify (Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [18.x, 20.x, 22.x] + node: [22, 24, 26] steps: - - name: Checkout - uses: actions/checkout@v4 - - - - name: Use Node ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 with: version: 9.15.4 - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: PNPM - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm run build - - lint: - name: Lint + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm lint + - run: pnpm test + - run: pnpm test:e2e + ci: + name: CI + if: always() + needs: [verify] runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '22.x' - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: PNPM - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm run lint - - - name: Validate Packages - run: pnpm manypkg check - - test: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - project: ['doubles.jest', 'doubles.sinon', 'core.unit', 'doubles.vitest', 'di.nestjs', 'di.inversify', 'unit'] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '22.x' - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: PNPM - run: pnpm install --frozen-lockfile - - - name: Create Coverage Directory - run: mkdir -p ${{ github.workspace }}/coverage - - - name: Test - run: pnpm --filter @contractual/${{ matrix.project }} run test + - name: Require every verification job env: - JEST_JUNIT_OUTPUT_NAME: ${{ matrix.project }}.xml - JEST_JUNIT_OUTPUT_DIR: ${{ github.workspace }}/test-reports - JUNIT_OUTPUT_NAME: ${{ matrix.project }} - JUNIT_OUTPUT_DIR: ${{ github.workspace }}/test-reports - COVERAGE_DIR: ${{ github.workspace }}/coverage - COVERAGE_FILE: coverage-${{ matrix.project }}.xml - - - name: Tests Results - uses: dorny/test-reporter@v1 - if: always() - with: - reporter: 'jest-junit' - name: Tests Results (${{ matrix.project }}) - path: ${{ github.workspace }}/test-reports/${{ matrix.project }}.xml - fail-on-error: false - -# - name: Upload Report to Codecov -# uses: codecov/codecov-action@v3 -# with: -# name: codecov-umbrella -# flags: ${{ matrix.project }} -# token: ${{ secrets.CODECOV_TOKEN }} -# fail_ci_if_error: true -# files: ${{ github.workspace }}/coverage/coverage-${{ matrix.project }}.xml -# verbose: true + RESULT: ${{ needs.verify.result }} + run: test "$RESULT" = success diff --git a/.github/workflows/e2e-release.yml b/.github/workflows/e2e-release.yml new file mode 100644 index 0000000..9b3dfe2 --- /dev/null +++ b/.github/workflows/e2e-release.yml @@ -0,0 +1,158 @@ +name: E2E Release Simulation +env: + CI: true + +on: + workflow_call: + inputs: + target_branch: + description: 'Branch to test release from' + required: true + type: string + workflow_dispatch: + inputs: + target_branch: + description: 'Branch to test release from' + required: true + type: string + default: 'next' + +permissions: + contents: read + +jobs: + e2e: + if: github.ref == 'refs/heads/next' + name: E2E (${{ matrix.e2e-project }}, Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + e2e-project: ['cli-basic', 'cli-lifecycle', 'packages-import'] + node-version: [22.x, 24.x, 26.x] + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: next + fetch-depth: 0 + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 9.15.4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Start Verdaccio + run: docker compose -f local-e2e.docker-compose.yaml up -d verdaccio + + - name: Wait for Verdaccio + run: | + for i in {1..30}; do + if curl -s http://localhost:4873 > /dev/null; then + echo "Verdaccio is ready" + break + fi + echo "Waiting for Verdaccio..." + sleep 1 + done + + - name: Build + run: pnpm build + + - name: Setup Registry + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + registry-url: http://localhost:4873 + scope: '@contractual' + always-auth: false + + - name: Install jq + run: sudo apt-get install -y jq + + - name: Config Git + run: | + git config --global user.email "e2e@contractual.dev" + git config --global user.name "Contractual e2e" + + - name: Remove provenance from package.json files + run: | + find packages -name 'package.json' | while read filename; do + jq 'del(.publishConfig.provenance)' "$filename" > temp.json && mv temp.json "$filename" + done + + - name: Commit Provenance Removal + run: | + git add . + git commit -am "chore: remove provenance for e2e" + + - name: Version Packages + run: | + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + npx lerna version --yes \ + --conventional-commits \ + --conventional-prerelease \ + --preid e2e \ + --no-changelog \ + --allow-branch "$BRANCH_NAME" \ + --no-git-tag-version \ + --no-push \ + --force-publish \ + --no-commit-hooks + + - name: Commit Packages Versions + run: | + git add . + git commit -am "bump versions" + + - name: Capture Versions for Report + run: | + echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Project:** \`${{ matrix.e2e-project }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Node:** \`${{ matrix.node-version }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Versions:" >> $GITHUB_STEP_SUMMARY + lerna ls --json | jq -r '.[] | "- **\(.name)** -> `v\(.version)`"' >> $GITHUB_STEP_SUMMARY + + - name: Publish Packages to Verdaccio + run: | + npx lerna publish from-package --yes \ + --no-git-tag-version \ + --no-push \ + --no-git-reset \ + --exact \ + --dist-tag e2e + + - name: Clean Source (simulate fresh install) + run: | + rm -rf packages + rm -rf node_modules + rm pnpm-lock.yaml + rm package.json + rm -f .npmrc + + - name: Install E2E Dependencies from Verdaccio + run: | + cd "${{ github.workspace }}/e2e/${{ matrix.e2e-project }}" + npm install --registry http://localhost:4873 --no-package-lock + + - name: Execute Tests + run: | + cd "${{ github.workspace }}/e2e/${{ matrix.e2e-project }}" + npm test + + - name: Test Success Summary + if: success() + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Test Passed" >> $GITHUB_STEP_SUMMARY + echo "Packages work correctly when published" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..31cdbe1 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,39 @@ +name: Pull request title +on: + pull_request: + branches: [next] + types: [opened, edited, synchronize, reopened, ready_for_review] +permissions: + pull-requests: read +jobs: + title: + name: PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + scopes: | + cli + changesets + types + governance + differs\.core + differs\.json-schema + differs\.openapi + \* + requireScope: false + subjectPattern: '^\S[^\r\n]*$' diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index f104219..146f890 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -1,164 +1,133 @@ name: Publish Packages -env: - CI: true - -permissions: - id-token: write - contents: write - on: workflow_dispatch: inputs: - dist_tag: - description: 'Distribution Tag' - type: choice - options: - - 'next' - - 'latest' - - 'rc' - - 'dev' - - 'alpha' - - 'beta' - required: true - default: 'dev' - strategy: - description: 'Release Strategy' - type: choice - options: - - 'from-git' - - 'from-package' + commit: + description: Full reviewed commit SHA on next, explicitly approved by Omer required: true - default: 'from-package' - + type: string +permissions: + contents: read +concurrency: + group: release + cancel-in-progress: false jobs: - e2e: - name: Build and Test + publish: + if: github.ref == 'refs/heads/next' + environment: release runs-on: ubuntu-latest - strategy: - matrix: - e2e-project: ['jest/nestjs', 'sinon/nestjs', 'vitest/nestjs', 'jest/inversify', 'sinon/inversify', 'vitest/inversify'] - node-version: [16.x, 18.x, 20.x] - exclude: - - e2e-project: 'vitest/inversify' - node-version: '16.x' - - e2e-project: 'vitest/nestjs' - node-version: '16.x' + permissions: + id-token: write + contents: read + env: + APPROVED_COMMIT: ${{ inputs.commit }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: - ref: ${{ github.event.inputs.target_branch }} - - - name: Remove Vitest If Needed - if: ${{ matrix.node-version == '16.x' }} + ref: next + fetch-depth: 0 + persist-credentials: false + - name: Verify and select the exact approved commit run: | - rm -rf packages/doubles/vitest - git config --global user.email "ci@suites.dev" - git config --global user.name "Suites CI" - git add . - git commit -am "remove vitest temp" - - - name: Setup Node ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - name: Install pnpm - uses: pnpm/action-setup@v4 + [[ "$APPROVED_COMMIT" =~ ^[0-9a-f]{40}$ ]] + git merge-base --is-ancestor "$APPROVED_COMMIT" origin/next + git switch --detach "$APPROVED_COMMIT" + test "$(git rev-parse HEAD)" = "$APPROVED_COMMIT" + - uses: pnpm/action-setup@v6 with: version: 9.15.4 - - - name: PNPM - run: pnpm install --frozen-lockfile - - - name: Run Verdaccio Docker - run: | - docker run -d --name verdaccio \ - -p 4873:4873 \ - -v ${{ github.workspace }}/e2e/config.yaml:/verdaccio/conf/config.yaml \ - verdaccio/verdaccio - - - name: Build - run: pnpm build - - - name: Setup Registry - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: - node-version: ${{ matrix.node-version }} - registry-url: http://localhost:4873 - scope: '@suites' - always-auth: false - - - name: Install jq - run: sudo apt-get install jq - - - name: Remove provenance from publishConfig - run: | - find packages -name 'package.json' | while read filename; do - jq 'del(.publishConfig.provenance)' "$filename" > temp.json && mv temp.json "$filename" - done - - - name: Commit Change - run: | - git config --global user.email "e2e@suites.dev" - git config --global user.name "Suites e2e" - git add . - git commit -am "remove provenance" - - - name: Publish Packages - run: | - pnpm publish -r \ - --no-git-checks \ - --access public \ - --tag ci \ - --force - - name: Setup and Test - run: | - IFS='/' read -r library framework <<< "${{ matrix.e2e-project }}" - echo "FRAMEWORK=$framework" >> $GITHUB_ENV - echo "LIBRARY=$library" >> $GITHUB_ENV - - - name: Clean Source - run: | - rm -rf packages - rm -rf node_modules - - - name: Install Dependencies + node-version: 22 + registry-url: https://registry.npmjs.org/ + - run: pnpm install --frozen-lockfile + - name: Audit dependencies (two approved exceptions expire October 22) + env: + AUDIT_EXCEPTIONS_EXPIRE_AT: '2026-10-22T05:37:00Z' run: | - cd "$PWD/e2e/$LIBRARY/$FRAMEWORK" - npm install --no-cache --no-package-lock - npm install --dev --no-package-lock @types/node@${{ matrix.node-version }} - - - name: Execute Test + # Omer approved these publishing-gate exceptions on 2026-10-08. + # This does not approve dispatching publication, creating tags, or publishing packages. + # braces: CLI glob size/depth guards mitigate input; tooling remains affected. + # http-cache-semantics: reviewed Lerna callers use private caches on fresh runners. + # Remove exceptions when patched; expiry never rolls forward automatically. + report="$RUNNER_TEMP/dependency-audit.json" + audit_status=0 + pnpm audit --json > "$report" || audit_status=$? + cat "$report" + # Registry errors, muted findings, unexpected statuses/schema must fail closed. + jq -e --argjson status "$audit_status" ' + type == "object" and (has("error") | not) and .muted == [] and + (.advisories | type == "object") and + (.metadata.vulnerabilities | keys == ["critical", "high", "info", "low", "moderate"]) and + (.metadata.vulnerabilities | all(.[]; type == "number" and . >= 0)) and + (([.metadata.vulnerabilities[]] | add) == (.advisories | length)) and + (($status == 0 and (.advisories | length) == 0) or + ($status == 1 and (.advisories | length) > 0)) and + (.advisories | all(.[]; + (.module_name | type == "string") and + (.findings | type == "array" and length > 0) and + (.findings | all(.[]; (.version | type == "string") and + (.paths | type == "array" and length > 0) and + (.paths | all(.[]; type == "string")))))) + ' "$report" > /dev/null + jq --arg expires "$AUDIT_EXCEPTIONS_EXPIRE_AT" --argjson now "$(date +%s)" ' + [.advisories[] | select( + ($now < ($expires | fromdateiso8601) and + ((.github_advisory_id == "GHSA-vfj7-8cjw-p6xm" and + .module_name == "braces" and all(.findings[]; .version == "3.0.3")) or + (.github_advisory_id == "GHSA-ch52-4w7c-c8xp" and + .module_name == "http-cache-semantics" and + all(.findings[]; .version == "4.2.0" and + all(.paths[]; startswith(". > lerna@9.0.7 > ")))))) | not)] + ' "$report" > "$RUNNER_TEMP/blocking-advisories.json" + if ! jq -e 'length == 0' "$RUNNER_TEMP/blocking-advisories.json" > /dev/null; then + cat "$RUNNER_TEMP/blocking-advisories.json" + echo "::error::Audit contains unapproved findings or the temporary exceptions have expired." + exit 1 + fi + if jq -e '.advisories | length > 0' "$report" > /dev/null; then + echo "::warning::Audit passed with approved risk exceptions until $AUDIT_EXCEPTIONS_EXPIRE_AT; dependencies remain affected." + echo "Audit: temporary risk accepted for the reported braces/http-cache-semantics findings until $AUDIT_EXCEPTIONS_EXPIRE_AT. Not release approval." >> "$GITHUB_STEP_SUMMARY" + fi + - run: pnpm build + - run: pnpm lint + - run: pnpm test + - run: pnpm test:e2e + - name: Verify beta metadata, immutable tags, and retry ownership + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - cd "$PWD/e2e/$LIBRARY/$FRAMEWORK" - npm test - - publish: - name: Publish Packages - needs: [e2e] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Registry - uses: actions/setup-node@v4 - with: - registry-url: https://registry.npmjs.org/ - scope: '@suites' - always-auth: true - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.15.4 - - - name: pnpm - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm build - - - name: Publish Packages - run: pnpm publish -r ${{ github.event.inputs.strategy }} --access public --tag ${{ github.event.inputs.dist_tag }} + test -n "$NODE_AUTH_TOKEN" + npm_user="$(npm whoami --registry=https://registry.npmjs.org/)" + package_access="$(npm access list packages "$npm_user" --json)" + for manifest in packages/*/package.json; do + if jq -e '.private == true' "$manifest" > /dev/null; then continue; fi + jq -e '.repository.url == "https://github.com/codotech/contractual.git" and .publishConfig.access == "public" and (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+-beta\\.[0-9]+$"))' "$manifest" > /dev/null + package_name="$(jq -r '.name' "$manifest")" + version="$(jq -r '.version' "$manifest")" + tag="$package_name@$version" + jq -e --arg name "$package_name" '.[$name] == "read-write"' <<< "$package_access" > /dev/null + if git show-ref --verify --quiet "refs/tags/$tag"; then + test "$(git rev-parse "refs/tags/$tag^{commit}")" = "$APPROVED_COMMIT" + fi + if npm view "$package_name@$version" --json > "$RUNNER_TEMP/registry-package.json"; then + jq -e --arg sha "$APPROVED_COMMIT" '.gitHead == $sha' "$RUNNER_TEMP/registry-package.json" > /dev/null + test "$(npm view "$package_name" dist-tags.beta)" = "$version" + else + jq -e '.error.code == "E404"' "$RUNNER_TEMP/registry-package.json" > /dev/null + fi + echo "$tag: publishing preflight passed (beta)" + done + - name: Publish only to beta, never latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: pnpm exec lerna publish from-package --yes --dist-tag beta --git-head "$APPROVED_COMMIT" --no-git-reset + - name: Verify published metadata (tagging remains a separate approval) + run: | + for manifest in packages/*/package.json; do + if jq -e '.private == true' "$manifest" > /dev/null; then continue; fi + package_name="$(jq -r '.name' "$manifest")" + version="$(jq -r '.version' "$manifest")" + npm view "$package_name@$version" --json | jq -e --arg sha "$APPROVED_COMMIT" '.gitHead == $sha' > /dev/null + test "$(npm view "$package_name" dist-tags.beta)" = "$version" + done diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index c6c8cba..b2c2efd 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -3,88 +3,65 @@ on: workflow_dispatch: inputs: release_type: - description: 'Release Type' + description: Use preminor to enter beta, prerelease for the next beta type: choice - options: - - 'graduate' - - 'prerelease' + options: [preminor, prerelease] + default: preminor required: true - default: 'prerelease' - target_branch: - description: 'Target Branch' - type: choice - options: - - 'master' - - 'next' - required: true - default: 'next' preid: - description: 'Prefix Id' + description: Prerelease identifier type: choice - options: - - 'latest' - - 'next' - - 'rc' - - 'dev' - - 'alpha' - - 'beta' + options: [beta] + default: beta required: true - default: 'next' - exact_version: - description: 'Exact Version' - type: string - required: false - permissions: contents: write - id-token: write - + pull-requests: write +concurrency: + group: release + cancel-in-progress: false jobs: - tag-version: - name: Tag Version + prepare: + if: github.ref == 'refs/heads/next' + environment: release runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: - ref: ${{ github.event.inputs.target_branch }} + ref: next fetch-depth: 0 - - - name: Config Git User - run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" - - - name: Prerelease Version (Exact Version) - if: ${{ github.event.inputs.release_type == 'prerelease' && github.event.inputs.exact_version }} - run: | - pnpm version ${{ github.event.inputs.exact_version }} \ - --workspaces-update \ - --no-git-tag-version \ - --no-commit-hooks + persist-credentials: false + - uses: pnpm/action-setup@v6 + with: + version: 9.15.4 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm test + - run: pnpm test:e2e + - name: Prepare version changes without tags env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Prerelease Version - if: ${{ github.event.inputs.release_type == 'prerelease' && !github.event.inputs.exact_version }} + RELEASE_TYPE: ${{ inputs.release_type }} + PREID: ${{ inputs.preid }} run: | - pnpm version prerelease \ - --preid ${{ github.event.inputs.preid }} \ - --workspaces-update \ - --no-git-tag-version \ - --no-commit-hooks + test "$PREID" = beta + case "$RELEASE_TYPE" in preminor|prerelease) ;; *) exit 1 ;; esac + git switch -c "chore/release-${GITHUB_RUN_ID}" + pnpm exec lerna version "$RELEASE_TYPE" --preid "$PREID" --conventional-commits --yes --no-git-tag-version --no-push --no-commit-hooks --ignore-scripts --allow-branch "chore/release-${GITHUB_RUN_ID}" + pnpm install --lockfile-only --ignore-scripts + - name: Open a release PR env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Graduate Version - if: ${{ github.event.inputs.release_type == 'graduate' }} - run: pnpm version --workspaces-update from-git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Push Changes to Branch + GH_TOKEN: ${{ github.token }} run: | - git push origin ${{ github.event.inputs.target_branch }} --no-verify - git push origin --tags - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if git diff --quiet; then + echo 'No version changes to prepare.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add packages pnpm-lock.yaml + git -c core.hooksPath=/dev/null commit -m 'chore(*): prepare beta packages' + git -c credential.helper='!gh auth git-credential' push origin "HEAD:refs/heads/chore/release-${GITHUB_RUN_ID}" + gh pr create --base next --head "chore/release-${GITHUB_RUN_ID}" --title 'chore(*): prepare beta packages' --body 'Review package versions and validation before merging. This PR does not tag or publish. Publishing requires a separate explicit approval from Omer. Approve the bot PR workflow runs if GitHub requests it.' diff --git a/.github/workflows/release-preview.yml b/.github/workflows/release-preview.yml new file mode 100644 index 0000000..b6cf919 --- /dev/null +++ b/.github/workflows/release-preview.yml @@ -0,0 +1,51 @@ +name: Release Preview +on: + workflow_dispatch: + inputs: + release_type: + description: Use preminor to enter beta, prerelease for the next beta + type: choice + options: [preminor, prerelease] + default: preminor + required: true + preid: + description: Prerelease identifier + type: choice + options: [beta] + default: beta + required: true +permissions: + contents: read +jobs: + preview: + name: Preview Release + if: github.ref == 'refs/heads/next' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: next + fetch-depth: 0 + persist-credentials: false + - uses: pnpm/action-setup@v6 + with: + version: 9.15.4 + - uses: actions/setup-node@v7 + with: + node-version: 22 + # Deliberately do not read or write caches in release workflows. + - run: pnpm install --frozen-lockfile + - name: Preview versions without commits, tags, or publication + env: + RELEASE_TYPE: ${{ inputs.release_type }} + PREID: ${{ inputs.preid }} + run: | + test "$PREID" = beta + case "$RELEASE_TYPE" in preminor|prerelease) ;; *) exit 1 ;; esac + pnpm exec lerna version "$RELEASE_TYPE" --preid "$PREID" --conventional-commits --yes --no-git-tag-version --no-push --no-commit-hooks --ignore-scripts --allow-branch next + pnpm install --lockfile-only --ignore-scripts + - name: Summarize local preview + run: | + echo '## Proposed versions (preview only)' >> "$GITHUB_STEP_SUMMARY" + pnpm exec lerna ls --json | jq -r '.[] | "- **\(.name)**: \(.version)"' >> "$GITHUB_STEP_SUMMARY" + echo 'No commits, tags, or packages were published. Ask Omer before preparing or publishing a release.' >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..d25b5e6 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,73 @@ +name: Dependency security audit +on: + pull_request: + branches: [next] + push: + branches: [next] + schedule: + - cron: '30 6 * * 1' +permissions: + contents: read +jobs: + audit: + name: Dependency audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@v6 + with: + version: 9.15.4 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Install dependencies for complete audit paths + run: pnpm install --frozen-lockfile --ignore-scripts + - name: Audit dependencies (two approved exceptions expire October 22) + env: + AUDIT_EXCEPTIONS_EXPIRE_AT: '2026-10-22T05:37:00Z' + run: | + # Omer approved these two temporary risks on 2026-10-08, not a release. + # braces: CLI glob size/depth guards mitigate input; tooling remains affected. + # http-cache-semantics: reviewed Lerna callers use private caches on fresh runners. + # Remove exceptions when patched; expiry never rolls forward automatically. + report="$RUNNER_TEMP/dependency-audit.json" + audit_status=0 + pnpm audit --json > "$report" || audit_status=$? + cat "$report" + # Registry errors, muted findings, unexpected statuses/schema must fail closed. + jq -e --argjson status "$audit_status" ' + type == "object" and (has("error") | not) and .muted == [] and + (.advisories | type == "object") and + (.metadata.vulnerabilities | keys == ["critical", "high", "info", "low", "moderate"]) and + (.metadata.vulnerabilities | all(.[]; type == "number" and . >= 0)) and + (([.metadata.vulnerabilities[]] | add) == (.advisories | length)) and + (($status == 0 and (.advisories | length) == 0) or + ($status == 1 and (.advisories | length) > 0)) and + (.advisories | all(.[]; + (.module_name | type == "string") and + (.findings | type == "array" and length > 0) and + (.findings | all(.[]; (.version | type == "string") and + (.paths | type == "array" and length > 0) and + (.paths | all(.[]; type == "string")))))) + ' "$report" > /dev/null + jq --arg expires "$AUDIT_EXCEPTIONS_EXPIRE_AT" --argjson now "$(date +%s)" ' + [.advisories[] | select( + ($now < ($expires | fromdateiso8601) and + ((.github_advisory_id == "GHSA-vfj7-8cjw-p6xm" and + .module_name == "braces" and all(.findings[]; .version == "3.0.3")) or + (.github_advisory_id == "GHSA-ch52-4w7c-c8xp" and + .module_name == "http-cache-semantics" and + all(.findings[]; .version == "4.2.0" and + all(.paths[]; startswith(". > lerna@9.0.7 > ")))))) | not)] + ' "$report" > "$RUNNER_TEMP/blocking-advisories.json" + if ! jq -e 'length == 0' "$RUNNER_TEMP/blocking-advisories.json" > /dev/null; then + cat "$RUNNER_TEMP/blocking-advisories.json" + echo "::error::Audit contains unapproved findings or the temporary exceptions have expired." + exit 1 + fi + if jq -e '.advisories | length > 0' "$report" > /dev/null; then + echo "::warning::Audit passed with approved risk exceptions until $AUDIT_EXCEPTIONS_EXPIRE_AT; dependencies remain affected." + echo "Audit: temporary risk accepted for the reported braces/http-cache-semantics findings until $AUDIT_EXCEPTIONS_EXPIRE_AT. Not release approval." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/set-coverage.yml b/.github/workflows/set-coverage.yml index 4e9c09a..71a6426 100644 --- a/.github/workflows/set-coverage.yml +++ b/.github/workflows/set-coverage.yml @@ -1,66 +1,28 @@ name: Test & Coverage on: push: - branches: - - master - + branches: [next] permissions: - id-token: write contents: read - checks: write - jobs: test: - name: Test runs-on: ubuntu-latest - strategy: - matrix: - project: ['doubles.jest', 'doubles.sinon', 'core.unit', 'doubles.vitest', 'di.nestjs', 'di.inversify', 'unit'] steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v3 + - uses: actions/checkout@v7 with: - node-version: '18.x' - - - uses: actions/cache@v2 + persist-credentials: false + - uses: pnpm/action-setup@v6 with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: pnpm - run: pnpm install --frozen-lockfile - - - name: Create Coverage Directory - run: mkdir -p ${{ github.workspace }}/coverage - - - name: Test - run: pnpm --filter @contractual/${{ matrix.project }} run test - env: - JEST_JUNIT_OUTPUT_NAME: ${{ matrix.project }}.xml - JEST_JUNIT_OUTPUT_DIR: ${{ github.workspace }}/test-reports - JUNIT_OUTPUT_NAME: ${{ matrix.project }} - JUNIT_OUTPUT_DIR: ${{ github.workspace }}/test-reports - COVERAGE_DIR: ${{ github.workspace }}/coverage - COVERAGE_FILE: coverage-${{ matrix.project }}.xml - - - name: Tests Results - uses: dorny/test-reporter@v1 - if: always() + version: 9.15.4 + - uses: actions/setup-node@v7 with: - reporter: 'jest-junit' - name: Tests Results (${{ matrix.project }}) - path: ${{ github.workspace }}/test-reports/${{ matrix.project }}.xml - fail-on-error: false - - - name: Upload Report to Codecov - uses: codecov/codecov-action@v3 + node-version: 22 + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm exec vitest run --coverage + - name: Upload report to Codecov + uses: codecov/codecov-action@v7 with: - name: codecov-umbrella - flags: ${{ matrix.project }} token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - files: ${{ github.workspace }}/coverage/coverage-${{ matrix.project }}.xml - verbose: true \ No newline at end of file + fail_ci_if_error: false + files: coverage/cobertura-coverage.xml diff --git a/.github/workflows/tag-global.yml b/.github/workflows/tag-global.yml index 68079f2..cfe66db 100644 --- a/.github/workflows/tag-global.yml +++ b/.github/workflows/tag-global.yml @@ -4,30 +4,73 @@ on: workflow_dispatch: inputs: tag_name: - description: 'Tag Name (e.g. v1.0.0)' + description: 'Approved immutable beta tag (e.g. v0.2.0-beta.0)' required: true + commit: + description: 'Full reviewed commit SHA on next, explicitly approved by Omer' + required: true + type: string permissions: contents: write - id-token: write + +concurrency: + group: release + cancel-in-progress: false jobs: tag-and-push: + if: github.ref == 'refs/heads/next' + environment: release name: Tag and Push runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: + ref: next fetch-depth: 0 + persist-credentials: false + + - name: Verify approved commit and beta tag + env: + APPROVED_COMMIT: ${{ inputs.commit }} + TAG_NAME: ${{ inputs.tag_name }} + run: | + [[ "$APPROVED_COMMIT" =~ ^[0-9a-f]{40}$ ]] + [[ "$TAG_NAME" =~ ^(v|@contractual/[a-z.-]+@)[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]] + git check-ref-format "refs/tags/$TAG_NAME" + git merge-base --is-ancestor "$APPROVED_COMMIT" origin/next + git switch --detach "$APPROVED_COMMIT" + test "$(git rev-parse HEAD)" = "$APPROVED_COMMIT" + if [[ "$TAG_NAME" == @contractual/* ]]; then + package_name="${TAG_NAME%@*}" + version="${TAG_NAME##*@}" + jq -se --arg name "$package_name" --arg version "$version" 'any(.[]; .name == $name and .version == $version)' packages/*/package.json > /dev/null + fi + if git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then + test "$(git rev-parse "refs/tags/$TAG_NAME^{commit}")" = "$APPROVED_COMMIT" + fi - name: Config Git run: | - git config --global user.email "omer.moradd@gmail.com" - git config --global user.name "Omer Morad" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - name: Tag and Push + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ inputs.tag_name }} + APPROVED_COMMIT: ${{ inputs.commit }} + run: | + if ! git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then + git tag -a "$TAG_NAME" "$APPROVED_COMMIT" -m "Tag Version $TAG_NAME" + fi + git -c credential.helper='!gh auth git-credential' push origin "refs/tags/$TAG_NAME" + + - name: Generate Summary + env: + TAG_NAME: ${{ inputs.tag_name }} run: | - git tag -a ${{ inputs.tag_name }} -m "Tag Version ${{ inputs.tag_name }}" - git push origin ${{ inputs.tag_name }} \ No newline at end of file + echo "Tagged $TAG_NAME at $(git rev-parse HEAD). No packages were published." >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 515c51e..885b5cc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* +junit.xml + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9ac3f44 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Repository workflow + +- Start feature/fix branches from the latest `origin/next`; open PRs targeting `next`. +- Use Conventional Commit PR titles and commits, for example `fix(cli): handle missing snapshots`. +- Squash merge only after required CI and review. Never bypass the ruleset or push directly to `next`. +- Run `pnpm build`, `pnpm lint`, `pnpm test`, and `pnpm test:e2e` for release-related changes. +- Use existing GitHub Actions and native workflow commands; do not add helper scripts or custom package-verification actions. Manage rulesets directly in GitHub, not checked-in JSON copies. +- Always ask Omer for explicit approval before creating, moving, or pushing any version tag; publishing packages or GitHub Releases; changing npm distribution tags; or dispatching a workflow that performs those actions. Approval to fix code or open a PR does not authorize a release. +- Do not change package versions unless a release preparation was explicitly requested. Preserve existing untracked `docs/` material. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c7f8c1f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to Contractual + +All development targets `next`, the schema lifecycle implementation. `master` contains the previous code-generation product. + +```sh +git fetch origin +git switch -c fix/describe-the-change origin/next +pnpm install --frozen-lockfile +pnpm build +``` + +Use Node 22 or newer and pnpm 9.15.4. Before opening a PR, run: + +```sh +pnpm lint +pnpm test +pnpm test:e2e +``` + +These commands validate source and behavior. CI runs them on Node 22, 24, and 26. Normal CI never publishes or tags. Use existing GitHub Actions and native workflow commands, without a separate scripts folder or custom package-verification action. + +## Open a PR to next + +Use a Conventional Commit title: `fix(cli): handle missing snapshots`, `feat(differs.core): classify new constraints`, or `docs: clarify supported formats`. Supported types are `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, and `revert`. Use `!` before the colon for a breaking change. + +One approving review, resolved conversations, an up-to-date branch, and passing `CI` and `PR title` checks are required. PRs are squash merged, with the PR title used as the commit subject. The ruleset is managed directly in the repository's GitHub settings; no ruleset JSON file is needed. + +Scopes must match a workspace package: `cli`, `changesets`, `types`, `governance`, `differs.core`, `differs.json-schema`, or `differs.openapi`. Use `*` for changes across packages or omit the scope for repository-wide changes. The semantic PR action enforces this allowlist; update both its workflow configuration and the repository ruleset when adding or renaming a package. + +## Release approval + +Normal development never publishes packages or creates version tags. Ask Omer before tagging, publishing, changing npm distribution tags, or dispatching a release workflow. See [RELEASING.md](RELEASING.md) for the release procedure and current support boundary. diff --git a/README.md b/README.md index eeff71e..addcde6 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,111 @@ -

Contractual

- -Contractual is a tool for managing API and data schemas as structured contracts. It ensures that schemas -are defined, versioned, and enforced across teams, whether for REST APIs, event-driven systems, or structured data -exchanges. - -Common use cases include: \ -πŸ”Ή Keeping API Contracts in Sync Between Backend and Frontend \ -πŸ”Ή Generating Type-Safe Clients and Server Contracts \ -πŸ”Ή Preventing Breaking Changes and Detecting Schema Drift \ -πŸ”Ή Ensuring Consistency Between Backend and Data Teams \ -πŸ”Ή Generating Language-Specific Types from a Shared Contract - -By treating schemas as first-class entities, Contractual eliminates uncertainty at integration points, enabling backend, -frontend, and data engineering teams to maintain predictable and enforceable APIs and structured data across the entire -stack. - -> Initially built for the **Node.js and TypeScript ecosystem**, Contractual is planned to support additional -> languages. - -## πŸš€ In Practice - -### Install Contractual - -To get started, install the Contractual CLI globally: - -```bash -npm i -g @contractual/cli -``` - -### Initialize Your Project - -Run the `init` command to scaffold a new project: - -```bash -contractual init -``` - -This command creates the following project structure: - -``` -frontend/ # Your frontend application -server/ # Your server application -contractual/ # Contractual files -β”œβ”€β”€ api.tsp # TypeSpec API definition -β”œβ”€β”€ specs/ # OpenAPI auto-generated specs -``` - -> Contractual works seamlessly with **monorepos**, **monoliths**, and distributed repositories. - -### Define Your API - -Write your API definition in the `api.tsp` file. For example: - -```tsp -import "@typespec/http"; -import "@typespec/openapi"; -import "@typespec/openapi3"; - -using TypeSpec.Http; - -@service({ - title: "Petstore API", -}) -namespace PetstoreAPI; - -model Pet { - id: string; - name: string; -} - -@route("/pet") -@post -op addPet(@body body: Pet): Pet; -``` - -> You can experiment and validate your API definitions [using the TypeSpec playground](https://typespec.io/playground/) - -### Manage API Changes - -#### Save the Current State of Your API - -Run the `spec graduate` command to save the current state of your OpenAPI spec: - -```bash -contractual spec graduate -``` - -This will generate a new OpenAPI (3.1.0) YAML file with versioning, enabling to track API changes over time. The -updated structure will look like this: - -``` -contractual/ -β”œβ”€β”€ api.tsp # TypeSpec API definition -β”œβ”€β”€ specs/ # OpenAPI auto-generated specs -β”‚ β”œβ”€β”€ openapi-v1.0.0.yaml -client/ # Generated API clients -server/ # Server contracts -e2e/ # Type-safe API-driven tests -``` - -> You can track API evolution and changes easily with clear, versioned OpenAPI specs. - -Here’s a quick video showing how this works: - -
- -
- -### Generate Contracts - -Run the `contract generate` command to generate type-safe clients, server contracts, and updated OpenAPI specs: - -```bash -contractual contract generate -``` - -This command creates: - -- **Type-safe client libraries** [using **ts-rest**](https://ts-rest.com), integrated with **Zod** for runtime - validation. -- **Server contracts** for frameworks like **Express**, **Fastify**, and **NestJS**. -- **Updated OpenAPI specs**. - -Here’s a short video showing contract generation in action: - -
- -
-``` - -## πŸ” Why Contractual? - -Maintaining the consistency of schemas across various services presents significant challenges. As systems evolve, -type-definitions and schemas drift, unnoticed breaking changes occur, and different teams find it challenging to -synchronize. APIs, event schemas, and structured data formats often become disconnected from their original intent, -leading to brittle integrations, manual fixes, and unexpected failures. - -**Some of the biggest pain points teams face include:** - -- **Schema Drift & Misalignment:** APIs and data contracts become inconsistent across teams, leading to mismatches, broken integrations, and regressions. - -- **Untracked Changes & Breaking Updates:** Without tracking modifications, updates can unexpectedly break consumers, causing downtime and costly debugging. - -- **Scattered Schemas & Code Maintenance:** Outdated documentation and manually managed type definitions create unreliable integrations and make maintaining entity models error-prone. - -## πŸ”‘ The Contract-First Approach -Most teams take a **code-first** approach to API development, where schemas are generated after implementation. This often results in **misalignment between services, outdated documentation, and accidental breaking changes.** Backend teams define APIs, frontend teams consume them, and data engineers rely on structured data formatsβ€”all of which can drift over time when schemas are an afterthought. - -A **contract-first** approach flips this process: schemas are designed before any implementation begins, ensuring that API structures, event definitions, and data formats remain stable and predictable. This approach allows teams to: - -- Define schemas upfront and enforce them as the single source of truth. - -- Track changes and prevent breaking updates before they impact consumers. - -- Generate type-safe clients and server contracts in multiple languages, reducing friction between teams. - -## πŸ“˜ Roadmap - -Want to contribute? Check out the alpha version [Roadmap](https://github.com/contractual-dev/contractual/issues/8) and -join the journey! πŸš€ - -## ❀️ Join the Community - -Contractual is open-source, and we’re looking for contributors to help shape its future, if you’re interested in -collaborating, please reach out. - -πŸ“© **Feedback or Questions?** Reach out -via [GitHub Discussions](https://github.com/contractual-dev/contractual/discussions). - -## πŸ”’ License - -Licensed under [MIT](LICENSE). +

+ Contractual +

+ +

Contractual

+ +

+Schema contract lifecycle for OpenAPI and JSON Schema +
+Linting β€’ Breaking change detection β€’ Versioning β€’ Release automation +

+ +
+ license + PRs welcome + npm downloads +
+ +

+ Docs +   β€’   + Quickstart +   β€’   + Breaking Detection +   β€’   + GitHub Action +

+ +

+Supported Formats: OpenAPI, JSON Schema +

+ +## Features + +- **Structural Breaking Change Detection** - Compares specs against versioned snapshots using structural diffing, not string comparison. Catches removed fields, type changes, and endpoint deletions. + +- **Automated Versioning** - Changesets declare bump levels (major/minor/patch). `contractual version` consumes them, bumps versions, updates snapshots, and generates changelogs. + +- **CI Integration** - GitHub Action posts diff tables on PRs, auto-generates changesets, and opens Version PRs for release automation. + +- **Format Agnostic** - Works with OpenAPI and JSON Schema. Custom linters and differs can be configured per contract. + +## Quick Example + +### Detect changes + +```bash +$ contractual diff + +orders-api: 3 changes (2 breaking, 1 non-breaking) β€” suggested bump: major + + BREAKING Removed endpoint GET /orders/{id}/details + BREAKING Changed type of field 'amount': string β†’ number + non-breaking Added optional field 'tracking_url' +``` + +### Generate a changeset + +```bash +$ contractual changeset + +? Bump type for orders-api: major +? Summary: Remove deprecated endpoint, change amount type + +Wrote .contractual/changesets/fuzzy-lion-dances.md +``` + +### Bump versions + +```bash +$ contractual version + +orders-api 1.4.2 β†’ 2.0.0 (major) + +Updated .contractual/versions.json +Updated CHANGELOG.md +``` + +## Installation + +Development releases are currently available under the npm `dev` tag. There is no stable release yet. Built-in linting and diffing support OpenAPI and JSON Schema; AsyncAPI and ODCS require custom engines. AI, fixed versioning, and generation hooks are planned. See [release scope](RELEASING.md) and [contributing](CONTRIBUTING.md). + +```bash +npm install -g @contractual/cli@dev +``` + +Or with other package managers: + +```bash +pnpm add -g @contractual/cli@dev +yarn global add @contractual/cli@dev +``` + +## Getting Started + +1. **Initialize** - `contractual init` scans for specs and creates `contractual.yaml` +2. **Lint** - `contractual lint` validates specs +3. **Detect changes** - `contractual diff` shows all changes classified +4. **CI gate** - `contractual breaking` fails if breaking changes exist +5. **Version** - `contractual changeset` + `contractual version` for releases + +[β†’ Full Quickstart Guide](https://contractual.dev/getting-started/quickstart) + +## Community + +- [Documentation](https://contractual.dev) +- [GitHub Issues](https://github.com/codotech/contractual/issues) + +## License + +[MIT](LICENSE) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..f71e0f4 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,23 @@ +# Releasing Contractual + +Releases require Omer's explicit approval. Fixes and merged PRs do not authorize tagging or publishing. The `release` environment requires Omer's approval for release jobs. + +## Supported release scope + +The first stable scope is OpenAPI 3.0/3.1 and JSON Schema linting and diffing, changesets, independent versioning, and the GitHub Action. AsyncAPI and ODCS can be tracked and versioned but need explicit custom lint/diff commands or disabled checks. They have no built-in governance engines. Fixed versioning, AI features, and generation hooks are not implemented; do not advertise them as stable functionality. + +## Prepare a reviewed release + +1. Run all checks in [CONTRIBUTING.md](CONTRIBUTING.md). +2. Present Omer with the exact commit, proposed package versions, npm distribution tag, and validation results. Obtain approval before preparing version changes or running a release workflow. +3. Run **Prepare Release** from `next`. It opens a version PR targeting `next`; it does not create tags or publish. Review and merge that PR normally. +4. Obtain approval for the exact merged release commit, then run **Publish Packages** with that full commit SHA and the approved distribution tag. `latest` rejects prerelease versions. Approve the `release` environment job in GitHub. +5. Verify package installations and imports from npm. Update the Action's pinned dependencies, rebuild its committed bundle, and review the Action PR before requesting approval for an Action tag or GitHub Release. + +Tag creation is separate and manual. Do not move existing development tags or publish a stable major alias without approval. The old `next β†’ master` PR is historical and is not a prerequisite for releasing from `next`. + +## Publishing failures + +The June 18 attempt left CLI `0.1.0-dev.8` and changesets `0.1.0-dev.6` tagged but unpublished. The last npm CLI release is `0.1.0-dev.7` under `dev`; `latest` still points to `0.1.0-dev.0`. Do not repair these registry tags without approval. + +Publishing preflight checks authentication, package write access, repository metadata, and version/tag compatibility before running Lerna. If npm returns `E404`, verify the configured `NPM_TOKEN` can write every `@contractual` package and that npm access and provenance match `codotech/contractual`. Never print a token in logs. A successful build is not evidence of npm authorization. diff --git a/contract-generate.gif b/contract-generate.gif deleted file mode 100644 index acc7946..0000000 Binary files a/contract-generate.gif and /dev/null differ diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/e2e/cli-basic/cli-install.test.ts b/e2e/cli-basic/cli-install.test.ts new file mode 100644 index 0000000..3ebfed7 --- /dev/null +++ b/e2e/cli-basic/cli-install.test.ts @@ -0,0 +1,79 @@ +import { execSync } from 'node:child_process'; +import { describe, test, expect } from 'vitest'; + +function run(command: string): string { + return execSync(command, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }, + }); +} + +describe('CLI Installation and Basic Commands', () => { + test('contractual binary is available after npm install', () => { + const result = run('npx @contractual/cli --version'); + expect(result).toMatch(/\d+\.\d+\.\d+/); + }); + + test('contractual --help shows available commands', () => { + const result = run('npx @contractual/cli --help'); + expect(result).toContain('init'); + expect(result).toContain('lint'); + expect(result).toContain('diff'); + expect(result).toContain('breaking'); + expect(result).toContain('changeset'); + expect(result).toContain('version'); + expect(result).toContain('status'); + expect(result).toContain('contract'); + expect(result).toContain('pre'); + }); + + test('contractual init --help shows init options', () => { + const result = run('npx @contractual/cli init --help'); + expect(result.toLowerCase()).toContain('initialize'); + }); + + test('contractual lint --help shows lint options', () => { + const result = run('npx @contractual/cli lint --help'); + expect(result).toContain('--format'); + }); + + test('contractual breaking --help shows breaking options', () => { + const result = run('npx @contractual/cli breaking --help'); + expect(result).toContain('--format'); + }); + + test('contractual diff --help shows diff options', () => { + const result = run('npx @contractual/cli diff --help'); + expect(result).toContain('--format'); + expect(result).toContain('--severity'); + expect(result).toContain('--verbose'); + }); + + test('contractual contract --help shows subcommands', () => { + const result = run('npx @contractual/cli contract --help'); + expect(result).toContain('add'); + expect(result).toContain('list'); + }); + + test('contractual contract add --help shows add options', () => { + const result = run('npx @contractual/cli contract add --help'); + expect(result).toContain('--name'); + expect(result).toContain('--type'); + expect(result).toContain('--path'); + }); + + test('contractual pre --help shows subcommands', () => { + const result = run('npx @contractual/cli pre --help'); + expect(result).toContain('enter'); + expect(result).toContain('exit'); + expect(result).toContain('status'); + }); + + test('contractual version --help shows version options', () => { + const result = run('npx @contractual/cli version --help'); + expect(result).toContain('--dry-run'); + expect(result).toContain('--json'); + expect(result).toContain('--yes'); + }); +}); diff --git a/e2e/cli-basic/package.json b/e2e/cli-basic/package.json new file mode 100644 index 0000000..12385d2 --- /dev/null +++ b/e2e/cli-basic/package.json @@ -0,0 +1,20 @@ +{ + "name": "contractual-e2e-cli-basic", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "@contractual/cli": "e2e" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "vitest": "^4.1.11", + "typescript": "~5.7.2" + }, + "scripts": { + "test": "vitest run" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/e2e/cli-basic/tsconfig.json b/e2e/cli-basic/tsconfig.json new file mode 100644 index 0000000..18f6945 --- /dev/null +++ b/e2e/cli-basic/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/e2e/cli-basic/vitest.config.ts b/e2e/cli-basic/vitest.config.ts new file mode 100644 index 0000000..df1ce1b --- /dev/null +++ b/e2e/cli-basic/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['**/*.test.ts'], + testTimeout: 30_000, + globals: true, + }, +}); diff --git a/e2e/cli-lifecycle/fixtures/json-schema/order-base.json b/e2e/cli-lifecycle/fixtures/json-schema/order-base.json new file mode 100644 index 0000000..cec8f1b --- /dev/null +++ b/e2e/cli-lifecycle/fixtures/json-schema/order-base.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/e2e/cli-lifecycle/fixtures/json-schema/order-field-removed.json b/e2e/cli-lifecycle/fixtures/json-schema/order-field-removed.json new file mode 100644 index 0000000..4b8cb38 --- /dev/null +++ b/e2e/cli-lifecycle/fixtures/json-schema/order-field-removed.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/e2e/cli-lifecycle/fixtures/json-schema/order-optional-field-added.json b/e2e/cli-lifecycle/fixtures/json-schema/order-optional-field-added.json new file mode 100644 index 0000000..bb1166e --- /dev/null +++ b/e2e/cli-lifecycle/fixtures/json-schema/order-optional-field-added.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + }, + "tracking_number": { + "type": "string", + "description": "Shipment tracking number" + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/e2e/cli-lifecycle/full-lifecycle.test.ts b/e2e/cli-lifecycle/full-lifecycle.test.ts new file mode 100644 index 0000000..63189ee --- /dev/null +++ b/e2e/cli-lifecycle/full-lifecycle.test.ts @@ -0,0 +1,306 @@ +import { describe, test, expect, afterEach } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + run, + setupRepoWithConfig, + copyFixture, + writeFile, + readJSON, + fileExists, + listFiles, +} from './helpers.js'; + +describe('Full CLI Lifecycle (installed from Verdaccio)', () => { + let repo: { dir: string; cleanup: () => void }; + + afterEach(() => { + repo?.cleanup(); + }); + + test('init detects JSON Schema and creates config', () => { + repo = createTempRepo(); + const { dir } = repo; + + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.schema.json')); + + const result = run('init', dir); + expect(result.exitCode).toBe(0); + expect(fileExists(dir, 'contractual.yaml')).toBe(true); + expect(fileExists(dir, '.contractual/versions.json')).toBe(true); + }); + + test('status shows contract versions', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.2.3', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('status', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order/); + expect(result.stdout).toMatch(/1\.2\.3/); + }); + + test('breaking detects field removal', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + const parsed = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + }); + + test('changeset creates a changeset file for breaking change', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThan(0); + }); + + test('complete lifecycle: changeset -> version', () => { + repo = createTempRepo(); + const { dir } = repo; + + // Setup with a JSON Schema contract + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Make a breaking change + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + // Run changeset + const changesetResult = run('changeset', dir); + expect(changesetResult.exitCode).toBe(0); + + // Verify changeset exists + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThan(0); + + // Run version + const versionResult = run('version', dir); + expect(versionResult.exitCode).toBe(0); + expect(versionResult.stdout).toMatch(/2\.0\.0/); + + // Verify version updated + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order'].version).toBe('2.0.0'); + }); + + test('diff shows changes between spec and snapshot', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.contracts).toBeDefined(); + expect(parsed.contracts.order).toBeDefined(); + expect(parsed.contracts.order.changes.length).toBeGreaterThan(0); + }); + + test('contract list shows configured contracts', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('contract list --json', dir); + expect(result.exitCode).toBe(0); + const contracts = JSON.parse(result.stdout); + expect(Array.isArray(contracts)).toBe(true); + expect(contracts[0].name).toBe('order'); + }); + + test('contract add adds new contract to config', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('contract add --name user --type json-schema --path schemas/user.json -y', dir); + expect(result.exitCode).toBe(0); + + const listResult = run('contract list --json', dir); + const contracts = JSON.parse(listResult.stdout); + expect(contracts.length).toBe(2); + expect(contracts.find((c: { name: string }) => c.name === 'user')).toBeDefined(); + }); + + test('pre enter/exit manages pre-release mode', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Enter pre-release mode + const enterResult = run('pre enter beta', dir); + expect(enterResult.exitCode).toBe(0); + expect(fileExists(dir, '.contractual/pre.json')).toBe(true); + + // Check status + const statusResult = run('pre status', dir); + expect(statusResult.exitCode).toBe(0); + expect(statusResult.stdout).toMatch(/beta/i); + + // Exit pre-release mode + const exitResult = run('pre exit', dir); + expect(exitResult.exitCode).toBe(0); + expect(fileExists(dir, '.contractual/pre.json')).toBe(false); + }); + + test('version --dry-run shows preview without changes', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create changeset + writeFile( + dir, + '.contractual/changesets/test.md', + `--- +"order": minor +--- + +Test change +` + ); + + const result = run('version --dry-run', dir); + expect(result.exitCode).toBe(0); + + // Verify version NOT changed + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order'].version).toBe('1.0.0'); + }); + + test('version --json outputs structured result', () => { + repo = createTempRepo(); + const { dir } = repo; + + setupRepoWithConfig(dir, [{ name: 'order', type: 'json-schema', path: 'schemas/order.json' }]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, '.contractual/snapshots/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create changeset + writeFile( + dir, + '.contractual/changesets/test.md', + `--- +"order": patch +--- + +Bug fix +` + ); + + const result = run('version --json', dir); + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(output.bumps).toBeDefined(); + expect(Array.isArray(output.bumps)).toBe(true); + }); +}); diff --git a/e2e/cli-lifecycle/helpers.ts b/e2e/cli-lifecycle/helpers.ts new file mode 100644 index 0000000..b844588 --- /dev/null +++ b/e2e/cli-lifecycle/helpers.ts @@ -0,0 +1,167 @@ +import { execSync } from 'node:child_process'; +import { + mkdtempSync, + mkdirSync, + writeFileSync, + readFileSync, + existsSync, + rmSync, + readdirSync, + cpSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +/** Path to the fixtures directory */ +const FIXTURES_DIR = new URL('./fixtures', import.meta.url).pathname; + +/** + * Create a temp directory that auto-cleans after test + */ +export function createTempRepo(): { dir: string; cleanup: () => void } { + const dir = mkdtempSync(path.join(tmpdir(), 'contractual-e2e-')); + + return { + dir, + cleanup: () => { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }, + }; +} + +/** + * Run a contractual CLI command in the given directory + */ +export function run( + command: string, + cwd: string, + options?: { expectFail?: boolean } +): { stdout: string; stderr: string; exitCode: number } { + const fullCmd = `npx @contractual/cli ${command}`; + + try { + const stdout = execSync(fullCmd, { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' }, + }); + return { stdout, stderr: '', exitCode: 0 }; + } catch (err: unknown) { + const error = err as { stdout?: string; stderr?: string; status?: number }; + if (!options?.expectFail) { + throw new Error( + `Command failed: ${fullCmd}\n` + + `Exit code: ${error.status}\n` + + `stdout: ${error.stdout}\n` + + `stderr: ${error.stderr}` + ); + } + return { + stdout: error.stdout ?? '', + stderr: error.stderr ?? '', + exitCode: error.status ?? 1, + }; + } +} + +/** + * Copy a fixture file to the temp repo + */ +export function copyFixture(fixturePath: string, destPath: string): void { + const src = path.join(FIXTURES_DIR, fixturePath); + const destDir = path.dirname(destPath); + + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + + cpSync(src, destPath); +} + +/** + * Write a file in the temp repo + */ +export function writeFile(repoDir: string, relativePath: string, content: string): void { + const fullPath = path.join(repoDir, relativePath); + const dir = path.dirname(fullPath); + + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(fullPath, content); +} + +/** + * Read a file from the temp repo + */ +export function readFile(repoDir: string, relativePath: string): string { + return readFileSync(path.join(repoDir, relativePath), 'utf-8'); +} + +/** + * Check if a file exists in the temp repo + */ +export function fileExists(repoDir: string, relativePath: string): boolean { + return existsSync(path.join(repoDir, relativePath)); +} + +/** + * List files in a directory within the temp repo + */ +export function listFiles(repoDir: string, relativePath: string): string[] { + const fullPath = path.join(repoDir, relativePath); + if (!existsSync(fullPath)) return []; + return readdirSync(fullPath); +} + +/** + * Read and parse JSON from the temp repo + */ +export function readJSON(repoDir: string, relativePath: string): unknown { + return JSON.parse(readFile(repoDir, relativePath)); +} + +/** + * Read and parse YAML from the temp repo + */ +export function readYAML(repoDir: string, relativePath: string): unknown { + const content = readFile(repoDir, relativePath); + return parseYaml(content); +} + +/** + * Setup a repo with a contractual.yaml config and .contractual directory + */ +export function setupRepoWithConfig( + repoDir: string, + contracts: Array<{ + name: string; + type: string; + path: string; + }> +): void { + const config = { + contracts: contracts.map((c) => ({ + name: c.name, + type: c.type, + path: c.path, + })), + }; + + writeFile(repoDir, 'contractual.yaml', stringifyYaml(config)); + + // Create .contractual directory structure + mkdirSync(path.join(repoDir, '.contractual/changesets'), { recursive: true }); + mkdirSync(path.join(repoDir, '.contractual/snapshots'), { recursive: true }); + + if (!fileExists(repoDir, '.contractual/versions.json')) { + writeFile(repoDir, '.contractual/versions.json', '{}'); + } +} diff --git a/e2e/cli-lifecycle/package.json b/e2e/cli-lifecycle/package.json new file mode 100644 index 0000000..c6a893d --- /dev/null +++ b/e2e/cli-lifecycle/package.json @@ -0,0 +1,21 @@ +{ + "name": "contractual-e2e-cli-lifecycle", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "@contractual/cli": "e2e" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "vitest": "^4.1.11", + "typescript": "~5.7.2", + "yaml": "^2.8.2" + }, + "scripts": { + "test": "vitest run" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/e2e/cli-lifecycle/tsconfig.json b/e2e/cli-lifecycle/tsconfig.json new file mode 100644 index 0000000..18f6945 --- /dev/null +++ b/e2e/cli-lifecycle/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/e2e/cli-lifecycle/vitest.config.ts b/e2e/cli-lifecycle/vitest.config.ts new file mode 100644 index 0000000..795884b --- /dev/null +++ b/e2e/cli-lifecycle/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['**/*.test.ts'], + testTimeout: 60_000, + hookTimeout: 30_000, + globals: true, + }, +}); diff --git a/e2e/config.yaml b/e2e/config.yaml new file mode 100644 index 0000000..f74202d --- /dev/null +++ b/e2e/config.yaml @@ -0,0 +1,24 @@ +storage: /verdaccio/storage + +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +packages: + '@contractual/*': + access: $all + publish: $all + unpublish: $all + '**': + access: $all + publish: $all + proxy: npmjs + +auth: + htpasswd: + file: ./htpasswd + algorithm: bcrypt + rounds: 10 + +logs: + - { type: stdout, format: pretty, level: http } diff --git a/e2e/packages-import/imports.test.ts b/e2e/packages-import/imports.test.ts new file mode 100644 index 0000000..4b247a1 --- /dev/null +++ b/e2e/packages-import/imports.test.ts @@ -0,0 +1,38 @@ +import { describe, test, expect } from 'vitest'; + +describe('Package Imports (ESM)', () => { + test('@contractual/types exports are defined', async () => { + const types = await import('@contractual/types'); + // TypeScript type check - if this compiles, types are exported correctly + expect(types).toBeDefined(); + }); + + test('@contractual/changesets exports versioning utilities', async () => { + const changesets = await import('@contractual/changesets'); + expect(changesets).toBeDefined(); + // Check for key exports + expect(typeof changesets).toBe('object'); + }); + + test('@contractual/governance exports are available', async () => { + const governance = await import('@contractual/governance'); + expect(governance).toBeDefined(); + expect(typeof governance).toBe('object'); + }); + + test('@contractual/differs.json-schema exports differ', async () => { + const differ = await import('@contractual/differs.json-schema'); + expect(differ).toBeDefined(); + expect(typeof differ).toBe('object'); + }); + + test('@contractual/governance/linters subpath export works', async () => { + const linters = await import('@contractual/governance/linters'); + expect(linters).toBeDefined(); + }); + + test('@contractual/governance/differs subpath export works', async () => { + const differs = await import('@contractual/governance/differs'); + expect(differs).toBeDefined(); + }); +}); diff --git a/e2e/packages-import/package.json b/e2e/packages-import/package.json new file mode 100644 index 0000000..c4babaf --- /dev/null +++ b/e2e/packages-import/package.json @@ -0,0 +1,23 @@ +{ + "name": "contractual-e2e-packages-import", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "@contractual/types": "e2e", + "@contractual/changesets": "e2e", + "@contractual/governance": "e2e", + "@contractual/differs.json-schema": "e2e" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "vitest": "^4.1.11", + "typescript": "~5.7.2" + }, + "scripts": { + "test": "tsc --noEmit && vitest run" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/e2e/packages-import/tsconfig.json b/e2e/packages-import/tsconfig.json new file mode 100644 index 0000000..18f6945 --- /dev/null +++ b/e2e/packages-import/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/e2e/packages-import/vitest.config.ts b/e2e/packages-import/vitest.config.ts new file mode 100644 index 0000000..df1ce1b --- /dev/null +++ b/e2e/packages-import/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['**/*.test.ts'], + testTimeout: 30_000, + globals: true, + }, +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..791b27e --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "exclude": ["node_modules", "**/node_modules"] +} diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..686ebce --- /dev/null +++ b/lerna.json @@ -0,0 +1,27 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "version": "independent", + "command": { + "publish": { + "registry": "https://registry.npmjs.org/", + "allowBranch": [ + "master", + "next" + ], + "message": "chore(*): release packages [skip ci]", + "conventionalCommits": true + }, + "version": { + "allowBranch": [ + "master", + "next" + ], + "message": "chore(*): version packages [skip ci]", + "conventionalCommits": true + } + }, + "npmClient": "pnpm", + "packages": [ + "packages/*" + ] +} diff --git a/local-e2e.docker-compose.yaml b/local-e2e.docker-compose.yaml new file mode 100644 index 0000000..c205944 --- /dev/null +++ b/local-e2e.docker-compose.yaml @@ -0,0 +1,32 @@ +name: contractual-local-e2e + +services: + verdaccio: + image: verdaccio/verdaccio + ports: + - '4873:4873' + volumes: + - ./e2e/config.yaml:/verdaccio/conf/config.yaml + - verdaccio-storage:/verdaccio/storage + networks: + - e2e-network + + executor: + image: node:22-alpine + working_dir: /workspace + volumes: + - .:/workspace + command: ['tail', '-f', '/dev/null'] + networks: + - e2e-network + depends_on: + - verdaccio + environment: + - NPM_CONFIG_PROVENANCE=false + +networks: + e2e-network: + driver: bridge + +volumes: + verdaccio-storage: diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..cde1ab0 Binary files /dev/null and b/logo.png differ diff --git a/package.json b/package.json index 0cec923..e854dd8 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,17 @@ "name": "contractual-monorepo", "private": true, "version": "0.0.0", - "license": "Apache-2.0", + "license": "MIT", "type": "module", "repository": { "type": "git", - "url": "https://github.com/contractual-dev/contractual.git" + "url": "https://github.com/codotech/contractual.git" }, "engines": { - "node": "^18.12.0 || >=20.0.0" + "node": ">=22.0.0" }, "bugs": { - "url": "https://github.com/contractual-dev/contractual.git" + "url": "https://github.com/codotech/contractual.git" }, "homepage": "https://contractual.dev", "contributors": [ @@ -22,60 +22,60 @@ } ], "scripts": { - "build": "pnpm -r run build", - "tester": "pnpm -r run test", - "lint": "pnpm -r run lint", - "prepare": "husky" + "build": "pnpm lerna run build --stream", + "build:watch": "pnpm -r run build:watch", + "test": "vitest run", + "test:e2e": "vitest run --config vitest.config.e2e.ts", + "test:e2e:watch": "vitest --config vitest.config.e2e.ts", + "lint": "pnpm lerna run lint --parallel", + "prepare": "husky", + "version:preview": "lerna changed --json | jq -r '.[] | \"\\(.name) β†’ v\\(.version)\"'", + "e2e:verdaccio:up": "docker compose -f local-e2e.docker-compose.yaml up -d", + "e2e:verdaccio:down": "docker compose -f local-e2e.docker-compose.yaml down -v" }, - "dependencies": { - "@manypkg/cli": "^0.21.4", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^6.7.5", - "@typescript-eslint/parser": "^6.7.5", - "@vitest/coverage-c8": "^0.33.0", - "@vitest/coverage-v8": "3.0.3", - "braces": "3.0.3", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.0.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-prettier": "^5.0.1", - "follow-redirects": "1.15.6", - "ip": "2.0.1", - "lerna": "^7.3.1", + "devDependencies": { + "@types/node": "^22.20.4", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitest/coverage-v8": "^4.1.11", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.5.6", + "husky": "^8.0.3", + "json-schema-typed": "^8.0.2", + "lerna": "^9.0.7", "lint-staged": "^14.0.1", - "madge": "^7.0.0", - "micromatch": "4.0.8", - "prettier": "^3.2.5", - "rimraf": "^5.0.5", - "rxjs": "^7.8.1", - "tar": "6.2.0", - "ts-jest": "^29.1.3", - "ts-node": "^10.9.1", - "typescript": "~5.7.2", - "vitest": "^3.0.3" + "prettier": "^3.9.9", + "rimraf": "^6.1.3", + "typescript": "~5.7.3", + "vitest": "^4.1.11", + "vite": "^7.3.5", + "yaml": "^2.8.3" }, - "workspaces": [ - "packages/generators/*", - "packages/providers/*", - "packages/types/*", - "packages/*" - ], "lint-staged": { "*.ts": [ "eslint --ext .ts --fix" ] }, - "jest-junit": { - "outputDirectory": "test-reports", - "ancestorSeparator": " β€Ί ", - "uniqueOutputName": "true", - "suiteNameTemplate": "{filepath}", - "classNameTemplate": "{classname}", - "titleTemplate": "{title}" - }, "packageManager": "pnpm@9.15.4", - "devDependencies": { - "husky": "^8.0.3" + "pnpm": { + "overrides": { + "micromatch@<4.0.8": "4.0.8", + "minimatch@>=9.0.0 <9.0.7": "9.0.7", + "yaml@>=2.0.0 <2.8.3": "^2.8.3", + "vite@>=7.0.0 <7.3.5": "^7.3.5", + "esbuild@>=0.27.3 <0.28.1": "0.28.1", + "tar@<7.5.21": "7.5.21", + "brace-expansion@>=5.0.0 <5.0.12": "5.0.12", + "js-yaml@>=4.0.0 <4.3.2": "4.3.2", + "axios@>=1.0.0 <1.20.0": "1.20.0", + "pacote@>=21.0.0 <21.5.1": "21.5.1", + "smol-toml@<1.9.0": "1.9.0", + "nx@>=22.0.0 <22.7.10": "22.7.10", + "source-map-js@>=1.0.0 <1.2.2": "1.2.2", + "postcss-selector-parser@<7.1.6": "7.1.6" + } } } diff --git a/packages/changesets/CHANGELOG.md b/packages/changesets/CHANGELOG.md new file mode 100644 index 0000000..f46c9da --- /dev/null +++ b/packages/changesets/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/changesets@0.1.0-dev.6...@contractual/changesets@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/changesets/changesets/consume.ts b/packages/changesets/changesets/consume.ts new file mode 100644 index 0000000..e7ee36b --- /dev/null +++ b/packages/changesets/changesets/consume.ts @@ -0,0 +1,110 @@ +/** + * Changeset consumption utilities + * Processes changesets for the version command + */ + +import type { BumpType, ChangesetFile } from '@contractual/types'; + +/** + * Bump type priority for comparison (higher = more significant) + */ +const BUMP_PRIORITY: Readonly> = { + major: 3, + minor: 2, + patch: 1, +} as const; + +/** + * Compare two bump types and return the higher priority one + */ +function higherBump(a: BumpType, b: BumpType): BumpType { + return BUMP_PRIORITY[a] >= BUMP_PRIORITY[b] ? a : b; +} + +/** + * Aggregate bumps from multiple changesets + * For each contract, the highest bump type wins + * + * @param changesets - Array of parsed changeset files + * @returns Map of contract name to aggregated bump type + */ +export function aggregateBumps( + changesets: readonly ChangesetFile[] +): Record { + const aggregated: Record = {}; + + for (const changeset of changesets) { + for (const [contract, bump] of Object.entries(changeset.bumps)) { + const existing = aggregated[contract]; + aggregated[contract] = existing ? higherBump(existing, bump) : bump; + } + } + + return aggregated; +} + +/** + * Extract the markdown section for a specific contract from changesets + * Combines all change descriptions for the contract across all changesets + * + * @param changesets - Array of parsed changeset files + * @param contractName - Name of the contract to extract changes for + * @returns Combined markdown content for the contract + */ +export function extractContractChanges( + changesets: readonly ChangesetFile[], + contractName: string +): string { + const sections: string[] = []; + + for (const changeset of changesets) { + // Only process changesets that affect this contract + if (!(contractName in changeset.bumps)) { + continue; + } + + // Extract the section for this contract from the body + const section = extractSectionFromBody(changeset.body, contractName); + if (section) { + sections.push(section); + } + } + + return sections.join('\n\n'); +} + +/** + * Extract a contract's section from a changeset body + * Looks for ## ContractName and extracts content until the next ## or end + */ +function extractSectionFromBody(body: string, contractName: string): string { + const lines = body.split('\n'); + const headerPattern = new RegExp(`^##\\s+${escapeRegex(contractName)}\\s*$`); + + let inSection = false; + const sectionLines: string[] = []; + + for (const line of lines) { + if (headerPattern.test(line)) { + inSection = true; + continue; + } + + if (inSection) { + // Check if we've hit the next section + if (line.startsWith('## ')) { + break; + } + sectionLines.push(line); + } + } + + return sectionLines.join('\n').trim(); +} + +/** + * Escape special regex characters in a string + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/packages/changesets/changesets/create.ts b/packages/changesets/changesets/create.ts new file mode 100644 index 0000000..b2801ea --- /dev/null +++ b/packages/changesets/changesets/create.ts @@ -0,0 +1,101 @@ +/** + * Changeset creation utilities + * Creates changeset files from diff results + */ + +import type { DiffResult, BumpType, ChangeSeverity } from '@contractual/types'; +import { generateChangesetName } from './naming.js'; + +/** + * Result of creating a changeset file + */ +export interface CreateChangesetResult { + /** Generated filename (e.g., "brave-tigers-fly.md") */ + filename: string; + /** Full content of the changeset file including frontmatter and body */ + content: string; +} + +/** + * Severity label mapping for display purposes + */ +const SEVERITY_LABELS: Record = { + breaking: 'BREAKING', + 'non-breaking': 'minor', + patch: 'patch', + unknown: 'unknown', +} as const; + +/** + * Map change severity to display label + */ +function severityToLabel(severity: ChangeSeverity): string { + return SEVERITY_LABELS[severity]; +} + +/** + * Determine the suggested bump type for a contract based on its changes + */ +function determineBumpType(result: DiffResult): BumpType { + if (result.summary.breaking > 0) { + return 'major'; + } + if (result.summary.nonBreaking > 0) { + return 'minor'; + } + return 'patch'; +} + +/** + * Create a changeset from diff results + * + * @param results - Array of diff results from comparing specs + * @param overrides - Optional map of contract name to bump type overrides + * @returns Object with filename and content for the changeset file + */ +export function createChangeset( + results: DiffResult[], + overrides?: Readonly> +): CreateChangesetResult { + const filename = `${generateChangesetName()}.md`; + + // Build YAML frontmatter + const bumps: Record = {}; + for (const result of results) { + if (result.changes.length === 0) { + continue; + } + const suggestedBump = determineBumpType(result); + bumps[result.contract] = overrides?.[result.contract] ?? suggestedBump; + } + + // Generate YAML frontmatter + const yamlLines = Object.entries(bumps).map( + ([contract, bump]) => `"${contract}": ${bump}` + ); + const frontmatter = `---\n${yamlLines.join('\n')}\n---`; + + // Generate markdown body with changes grouped by contract + const bodyParts: string[] = []; + + for (const result of results) { + if (result.changes.length === 0) { + continue; + } + + bodyParts.push(`## ${result.contract}`); + bodyParts.push(''); + + for (const change of result.changes) { + const label = severityToLabel(change.severity); + bodyParts.push(`- **[${label}]** ${change.message}`); + } + + bodyParts.push(''); + } + + const body = bodyParts.join('\n').trim(); + const content = `${frontmatter}\n\n${body}\n`; + + return { filename, content }; +} diff --git a/packages/changesets/changesets/index.ts b/packages/changesets/changesets/index.ts new file mode 100644 index 0000000..dbc6070 --- /dev/null +++ b/packages/changesets/changesets/index.ts @@ -0,0 +1,4 @@ +export * from './naming.js'; +export * from './create.js'; +export * from './read.js'; +export * from './consume.js'; diff --git a/packages/changesets/changesets/naming.ts b/packages/changesets/changesets/naming.ts new file mode 100644 index 0000000..b849edd --- /dev/null +++ b/packages/changesets/changesets/naming.ts @@ -0,0 +1,126 @@ +/** + * Changeset naming utilities + * Generates random three-word names for changeset files + */ + +const ADJECTIVES = [ + 'brave', + 'calm', + 'eager', + 'fancy', + 'gentle', + 'happy', + 'jolly', + 'kind', + 'lively', + 'mighty', + 'noble', + 'proud', + 'quick', + 'ready', + 'sharp', + 'swift', + 'tender', + 'vivid', + 'warm', + 'zesty', +] as const; + +const NOUNS = [ + 'bears', + 'cats', + 'dogs', + 'eagles', + 'foxes', + 'geese', + 'hawks', + 'ibis', + 'jays', + 'kites', + 'lions', + 'mice', + 'newts', + 'owls', + 'pandas', + 'quails', + 'ravens', + 'seals', + 'tigers', + 'wolves', +] as const; + +const VERBS = [ + 'fly', + 'run', + 'jump', + 'swim', + 'dance', + 'sing', + 'play', + 'rest', + 'hunt', + 'roam', + 'climb', + 'glide', + 'soar', + 'leap', + 'dash', + 'drift', + 'march', + 'prowl', + 'race', + 'sprint', +] as const; + +/** + * Get a random element from an array + * @throws {Error} If the array is empty + */ +function randomElement(array: readonly T[]): T { + if (array.length === 0) { + throw new Error('Cannot get random element from empty array'); + } + const index = Math.floor(Math.random() * array.length); + const element = array[index]; + // This assertion is safe because we've checked array.length > 0 and index is within bounds + return element as T; +} + +/** + * Generate a random three-word changeset name + * Format: adjective-noun-verb (e.g., "brave-tigers-fly") + */ +export function generateChangesetName(): string { + const adjective = randomElement(ADJECTIVES); + const noun = randomElement(NOUNS); + const verb = randomElement(VERBS); + + return `${adjective}-${noun}-${verb}`; +} + +/** + * Generate a unique changeset name that doesn't collide with existing names + * Retries with new random names until a unique one is found + * + * @param existingNames - Array of existing changeset names to avoid + * @param maxRetries - Maximum number of retries before giving up (default: 100) + * @returns A unique changeset name + * @throws Error if unable to generate a unique name after max retries + */ +export function generateUniqueChangesetName( + existingNames: string[], + maxRetries = 100 +): string { + const existingSet = new Set(existingNames); + + for (let i = 0; i < maxRetries; i++) { + const name = generateChangesetName(); + if (!existingSet.has(name)) { + return name; + } + } + + // Fallback: append timestamp to ensure uniqueness + const baseName = generateChangesetName(); + return `${baseName}-${Date.now()}`; +} diff --git a/packages/changesets/changesets/read.ts b/packages/changesets/changesets/read.ts new file mode 100644 index 0000000..441322b --- /dev/null +++ b/packages/changesets/changesets/read.ts @@ -0,0 +1,141 @@ +/** + * Changeset reading and parsing utilities + * Parses changeset files from the .contractual/changesets directory + */ + +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import type { BumpType, ChangesetFile } from '@contractual/types'; + +/** + * Valid bump type values + */ +const VALID_BUMP_TYPES = ['major', 'minor', 'patch'] as const; + +/** + * Error thrown when parsing a changeset file fails + */ +export class ChangesetParseError extends Error { + constructor( + message: string, + public readonly filename?: string + ) { + super(filename ? `Failed to parse changeset "${filename}": ${message}` : message); + this.name = 'ChangesetParseError'; + } +} + +/** + * Parsed changeset content (before wrapping with filename) + */ +export interface ParsedChangesetContent { + /** Map of contract name to bump type */ + bumps: Record; + /** Markdown body with change descriptions */ + body: string; +} + +/** + * Validate that a value is a valid bump type + */ +function isValidBumpType(value: unknown): value is BumpType { + return VALID_BUMP_TYPES.includes(value as BumpType); +} + +/** + * Parse a changeset file content into its components + * + * @param content - Raw content of the changeset file + * @returns Parsed bumps and body + * @throws {ChangesetParseError} If the content is malformed + */ +export function parseChangeset(content: string): ParsedChangesetContent { + const trimmed = content.trim(); + + // Check for frontmatter delimiter + if (!trimmed.startsWith('---')) { + throw new ChangesetParseError('Changeset must start with YAML frontmatter (---)'); + } + + // Find the closing frontmatter delimiter + const secondDelimiter = trimmed.indexOf('---', 3); + if (secondDelimiter === -1) { + throw new ChangesetParseError('Changeset frontmatter must be closed with ---'); + } + + // Extract YAML and body + const yamlContent = trimmed.slice(3, secondDelimiter).trim(); + const body = trimmed.slice(secondDelimiter + 3).trim(); + + // Parse YAML + let parsed: unknown; + try { + parsed = parseYaml(yamlContent); + } catch (yamlError) { + const message = yamlError instanceof Error ? yamlError.message : 'Invalid YAML'; + throw new ChangesetParseError(`Invalid YAML in frontmatter: ${message}`); + } + + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new ChangesetParseError('Changeset frontmatter must contain a YAML object'); + } + + // Validate and extract bumps + const bumps: Record = {}; + + for (const [key, value] of Object.entries(parsed as Record)) { + if (!isValidBumpType(value)) { + throw new ChangesetParseError( + `Invalid bump type "${String(value)}" for contract "${key}". Must be ${VALID_BUMP_TYPES.join(', ')}.` + ); + } + bumps[key] = value; + } + + return { bumps, body }; +} + +/** + * Read all changeset files from a directory + * + * @param changesetsDir - Path to the changesets directory + * @returns Array of parsed changeset files + */ +export async function readChangesets(changesetsDir: string): Promise { + let files: string[]; + + try { + files = await readdir(changesetsDir); + } catch (error) { + // Directory doesn't exist or can't be read - no changesets + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; + } + throw error; + } + + // Filter to only .md files + const mdFiles = files.filter((file) => file.endsWith('.md')); + + const changesets: ChangesetFile[] = []; + + for (const filename of mdFiles) { + const filePath = join(changesetsDir, filename); + const content = await readFile(filePath, 'utf-8'); + + try { + const { bumps, body } = parseChangeset(content); + changesets.push({ filename, path: filePath, bumps, body }); + } catch (error) { + // Re-throw with filename context + if (error instanceof ChangesetParseError) { + throw new ChangesetParseError(error.message, filename); + } + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new ChangesetParseError(message, filename); + } + } + + return changesets; +} diff --git a/packages/changesets/index.ts b/packages/changesets/index.ts new file mode 100644 index 0000000..4be46a2 --- /dev/null +++ b/packages/changesets/index.ts @@ -0,0 +1,31 @@ +// Changesets +export { + generateChangesetName, + generateUniqueChangesetName, +} from './changesets/naming.js'; +export { createChangeset, type CreateChangesetResult } from './changesets/create.js'; +export { + parseChangeset, + readChangesets, + ChangesetParseError, + type ParsedChangesetContent, +} from './changesets/read.js'; +export { aggregateBumps, extractContractChanges } from './changesets/consume.js'; + +// Versioning +export { + VersionManager, + PreReleaseManager, + VERSIONS_FILE, + SNAPSHOTS_DIR, + CHANGESETS_DIR, + PRE_RELEASE_FILE, + DEFAULT_VERSION, + SPEC_EXTENSIONS, + incrementVersion, + incrementVersionWithPreRelease, + VersionError, + type BumpOperationResult, +} from './versioning/manager.js'; +export { formatDate, appendChangelog } from './versioning/changelog.js'; +export { updateSpecVersion } from './versioning/spec-updater.js'; diff --git a/packages/changesets/package.json b/packages/changesets/package.json new file mode 100644 index 0000000..b2eb0ee --- /dev/null +++ b/packages/changesets/package.json @@ -0,0 +1,54 @@ +{ + "name": "@contractual/changesets", + "private": false, + "version": "0.1.0-beta.0", + "description": "Changeset creation, parsing, and versioning utilities for Contractual", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./changesets": { + "types": "./dist/changesets/index.d.ts", + "import": "./dist/changesets/index.js" + }, + "./versioning": { + "types": "./dist/versioning/index.d.ts", + "import": "./dist/versioning/index.js" + } + }, + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/changesets" + }, + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "pnpm rimraf dist", + "build": "tsc -p tsconfig.build.json" + }, + "files": [ + "dist" + ], + "dependencies": { + "@contractual/types": "workspace:*", + "semver": "^7.8.5", + "yaml": "^2.8.3" + }, + "devDependencies": { + "@types/semver": "^7.8.0", + "vitest": "^4.1.11" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/changesets/tsconfig.build.json b/packages/changesets/tsconfig.build.json new file mode 100644 index 0000000..59e8982 --- /dev/null +++ b/packages/changesets/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true, + "incremental": true + }, + "references": [ + { "path": "../types/tsconfig.build.json" } + ], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/changesets/tsconfig.json b/packages/changesets/tsconfig.json new file mode 100644 index 0000000..c256fc3 --- /dev/null +++ b/packages/changesets/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": false, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/changesets/versioning/changelog.ts b/packages/changesets/versioning/changelog.ts new file mode 100644 index 0000000..bdb7be6 --- /dev/null +++ b/packages/changesets/versioning/changelog.ts @@ -0,0 +1,74 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import type { BumpResult } from '@contractual/types'; + +/** + * Format a date as YYYY-MM-DD + * @param date - The date to format + * @returns The formatted date string + */ +export function formatDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * Format a single bump result as a changelog entry + */ +function formatBumpEntry(bump: BumpResult, date: Date): string { + const dateStr = formatDate(date); + const header = `## [${bump.contract}] v${bump.newVersion} - ${dateStr}`; + const changes = bump.changes.trim(); + + return `${header}\n\n${changes}`; +} + +/** + * Append version bump entries to CHANGELOG.md + * Creates the file if it doesn't exist. + * Inserts entries after the "# Changelog" heading, or at the top if no heading. + * @param changelogPath - Path to the CHANGELOG.md file + * @param bumps - Array of bump results to append + */ +export function appendChangelog(changelogPath: string, bumps: BumpResult[]): void { + if (bumps.length === 0) { + return; + } + + const date = new Date(); + const newEntries = bumps + .map((bump) => formatBumpEntry(bump, date)) + .join('\n\n'); + + if (!existsSync(changelogPath)) { + // Create new changelog with header and entries + const content = `# Changelog\n\n${newEntries}\n`; + writeFileSync(changelogPath, content, 'utf-8'); + return; + } + + const existingContent = readFileSync(changelogPath, 'utf-8'); + const changelogHeading = '# Changelog'; + const headingIndex = existingContent.indexOf(changelogHeading); + + let newContent: string; + + if (headingIndex !== -1) { + // Insert after the heading line + const afterHeading = headingIndex + changelogHeading.length; + const beforeHeading = existingContent.slice(0, afterHeading); + const afterContent = existingContent.slice(afterHeading); + + // Preserve any blank lines after heading, then insert entries + const leadingWhitespaceMatch = afterContent.match(/^(\r?\n)*/); + const leadingWhitespace = leadingWhitespaceMatch?.[0] ?? '\n\n'; + + newContent = `${beforeHeading}\n\n${newEntries}${leadingWhitespace.length > 2 ? leadingWhitespace : '\n\n'}${afterContent.trimStart()}`; + } else { + // No heading found, prepend entries with heading + newContent = `# Changelog\n\n${newEntries}\n\n${existingContent}`; + } + + writeFileSync(changelogPath, newContent, 'utf-8'); +} diff --git a/packages/changesets/versioning/index.ts b/packages/changesets/versioning/index.ts new file mode 100644 index 0000000..d6f8d13 --- /dev/null +++ b/packages/changesets/versioning/index.ts @@ -0,0 +1,2 @@ +export * from './manager.js'; +export * from './changelog.js'; diff --git a/packages/changesets/versioning/manager.ts b/packages/changesets/versioning/manager.ts new file mode 100644 index 0000000..9656d0c --- /dev/null +++ b/packages/changesets/versioning/manager.ts @@ -0,0 +1,371 @@ +import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'node:fs'; +import { join, extname } from 'node:path'; +import * as semver from 'semver'; +import type { VersionsFile, SimpleVersionEntry, BumpType, PreReleaseState } from '@contractual/types'; + +/** + * Default version for new contracts + */ +export const DEFAULT_VERSION = '0.0.0' as const; + +/** + * Supported spec file extensions for snapshot lookup + */ +export const SPEC_EXTENSIONS = ['.yaml', '.yml', '.json'] as const; + +/** + * Error thrown when version operations fail + */ +export class VersionError extends Error { + constructor( + message: string, + public readonly version?: string, + public readonly bumpType?: BumpType + ) { + super(message); + this.name = 'VersionError'; + } +} + +/** + * Result of a version bump operation + */ +export interface BumpOperationResult { + /** Version before the bump */ + oldVersion: string; + /** Version after the bump */ + newVersion: string; +} + +/** + * Increment a version by a bump type + * @param version - The current version string (must be valid semver) + * @param bumpType - The type of version bump (major, minor, patch) + * @returns The new version string + * @throws {VersionError} If the version is invalid or increment fails + */ +export function incrementVersion(version: string, bumpType: BumpType): string { + if (!semver.valid(version)) { + throw new VersionError(`Invalid semver version: ${version}`, version, bumpType); + } + const newVersion = semver.inc(version, bumpType); + if (!newVersion) { + throw new VersionError( + `Failed to increment version ${version} with type ${bumpType}`, + version, + bumpType + ); + } + return newVersion; +} + +/** + * Filename for the versions registry + */ +export const VERSIONS_FILE = 'versions.json' as const; + +/** + * Directory name for snapshots + */ +export const SNAPSHOTS_DIR = 'snapshots' as const; + +/** + * Directory name for changesets + */ +export const CHANGESETS_DIR = 'changesets' as const; + +/** + * Filename for pre-release state + */ +export const PRE_RELEASE_FILE = 'pre.json' as const; + +/** + * Manages contract versions and snapshots + */ +export class VersionManager { + private readonly versionsPath: string; + private readonly snapshotsDir: string; + private versions: VersionsFile; + + constructor(contractualDir: string) { + this.versionsPath = join(contractualDir, VERSIONS_FILE); + this.snapshotsDir = join(contractualDir, SNAPSHOTS_DIR); + this.versions = this.load(); + } + + /** + * Load versions.json from disk + * @throws {VersionError} If the file exists but contains invalid JSON + */ + private load(): VersionsFile { + if (!existsSync(this.versionsPath)) { + return {}; + } + + const content = readFileSync(this.versionsPath, 'utf-8'); + try { + const parsed: unknown = JSON.parse(content); + if (!this.isValidVersionsFile(parsed)) { + throw new VersionError(`Invalid versions.json format at ${this.versionsPath}`); + } + return parsed; + } catch (error) { + if (error instanceof VersionError) { + throw error; + } + const message = error instanceof Error ? error.message : 'Unknown error'; + throw new VersionError(`Failed to parse versions.json: ${message}`); + } + } + + /** + * Type guard to validate VersionsFile structure + */ + private isValidVersionsFile(value: unknown): value is VersionsFile { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false; + } + for (const entry of Object.values(value as Record)) { + if (!this.isValidVersionEntry(entry)) { + return false; + } + } + return true; + } + + /** + * Type guard to validate SimpleVersionEntry structure + */ + private isValidVersionEntry(value: unknown): value is SimpleVersionEntry { + if (typeof value !== 'object' || value === null) { + return false; + } + const entry = value as Record; + return typeof entry.version === 'string' && typeof entry.released === 'string'; + } + + /** + * Get current version for a contract + * @param contractName - The contract name to look up + * @returns The version string, or null if not found + */ + getVersion(contractName: string): string | null { + const entry = this.versions[contractName]; + return entry?.version ?? null; + } + + /** + * Get snapshot path for a contract + * @param contractName - The contract name to look up + * @returns The snapshot file path, or null if not found + */ + getSnapshotPath(contractName: string): string | null { + for (const ext of SPEC_EXTENSIONS) { + const snapshotPath = join(this.snapshotsDir, `${contractName}${ext}`); + if (existsSync(snapshotPath)) { + return snapshotPath; + } + } + + return null; + } + + /** + * Bump version, update versions.json, and copy spec to snapshots + * @param contractName - The contract name to bump + * @param bumpType - The type of version bump (major, minor, patch) + * @param specPath - Path to the current spec file to snapshot + * @returns The old and new version strings + * @throws {VersionError} If the bump operation fails + */ + bump(contractName: string, bumpType: BumpType, specPath: string): BumpOperationResult { + const currentEntry = this.versions[contractName]; + const oldVersion = currentEntry?.version ?? DEFAULT_VERSION; + + // Use the shared incrementVersion function for consistent error handling + const newVersion = incrementVersion(oldVersion, bumpType); + + // Update versions entry + this.versions[contractName] = { + version: newVersion, + released: new Date().toISOString(), + }; + + // Copy spec to snapshots directory + const ext = extname(specPath) || '.yaml'; + const snapshotPath = join(this.snapshotsDir, `${contractName}${ext}`); + copyFileSync(specPath, snapshotPath); + + // Save versions.json + this.save(); + + return { oldVersion, newVersion }; + } + + /** + * Set version for a contract (used for initial version setup) + * @param contractName - The contract name + * @param version - The version to set + * @param specPath - Path to the spec file to snapshot + */ + setVersion(contractName: string, version: string, specPath: string): void { + if (!semver.valid(version)) { + throw new VersionError(`Invalid semver version: ${version}`, version); + } + + // Update versions entry + this.versions[contractName] = { + version, + released: new Date().toISOString(), + }; + + // Copy spec to snapshots directory + const ext = extname(specPath) || '.yaml'; + const snapshotPath = join(this.snapshotsDir, `${contractName}${ext}`); + copyFileSync(specPath, snapshotPath); + + // Save versions.json + this.save(); + } + + /** + * Save versions.json to disk + */ + private save(): void { + const content = JSON.stringify(this.versions, null, 2); + writeFileSync(this.versionsPath, content, 'utf-8'); + } + + /** + * Get all contract versions + * @returns Map of contract names to versions + */ + getAllVersions(): Record { + const result: Record = {}; + for (const [name, entry] of Object.entries(this.versions)) { + result[name] = entry.version; + } + return result; + } +} + +/** + * Manages pre-release state + */ +export class PreReleaseManager { + private readonly prePath: string; + + constructor(contractualDir: string) { + this.prePath = join(contractualDir, PRE_RELEASE_FILE); + } + + /** + * Check if pre-release mode is active + */ + isActive(): boolean { + return existsSync(this.prePath); + } + + /** + * Get current pre-release state + * @returns The pre-release state, or null if not in pre-release mode + */ + getState(): PreReleaseState | null { + if (!this.isActive()) { + return null; + } + + try { + const content = readFileSync(this.prePath, 'utf-8'); + return JSON.parse(content) as PreReleaseState; + } catch { + return null; + } + } + + /** + * Enter pre-release mode + * @param tag - The pre-release tag (e.g., "alpha", "beta", "rc") + * @param versionManager - VersionManager to get current versions + */ + enter(tag: string, versionManager: VersionManager): void { + if (this.isActive()) { + throw new VersionError(`Already in pre-release mode. Run 'pre exit' first.`); + } + + // Validate tag + if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(tag)) { + throw new VersionError( + `Invalid pre-release tag: ${tag}. Must start with letter, contain only letters, numbers, and hyphens.` + ); + } + + const state: PreReleaseState = { + tag, + enteredAt: new Date().toISOString(), + initialVersions: versionManager.getAllVersions(), + }; + + writeFileSync(this.prePath, JSON.stringify(state, null, 2), 'utf-8'); + } + + /** + * Exit pre-release mode + */ + exit(): void { + if (!this.isActive()) { + throw new VersionError('Not in pre-release mode.'); + } + + unlinkSync(this.prePath); + } + + /** + * Get the pre-release tag + * @returns The tag, or null if not in pre-release mode + */ + getTag(): string | null { + const state = this.getState(); + return state?.tag ?? null; + } +} + +/** + * Increment a version with pre-release support + * @param version - The current version string + * @param bumpType - The type of version bump + * @param preReleaseTag - Optional pre-release tag (e.g., "beta") + * @returns The new version string + */ +export function incrementVersionWithPreRelease( + version: string, + bumpType: BumpType, + preReleaseTag?: string +): string { + if (!semver.valid(version)) { + throw new VersionError(`Invalid semver version: ${version}`, version, bumpType); + } + + const parsed = semver.parse(version); + if (!parsed) { + throw new VersionError(`Failed to parse version: ${version}`, version, bumpType); + } + + // If no pre-release tag, use normal increment + if (!preReleaseTag) { + return incrementVersion(version, bumpType); + } + + // If already a pre-release with the same tag, just bump the pre-release number + if (parsed.prerelease.length > 0 && parsed.prerelease[0] === preReleaseTag) { + const newVersion = semver.inc(version, 'prerelease', preReleaseTag); + if (!newVersion) { + throw new VersionError(`Failed to increment pre-release version`, version, bumpType); + } + return newVersion; + } + + // Otherwise, apply the bump type and start a new pre-release + const baseVersion = incrementVersion(version, bumpType); + return `${baseVersion}-${preReleaseTag}.0`; +} diff --git a/packages/changesets/versioning/spec-updater.malformed-yaml.test.ts b/packages/changesets/versioning/spec-updater.malformed-yaml.test.ts new file mode 100644 index 0000000..dda4829 --- /dev/null +++ b/packages/changesets/versioning/spec-updater.malformed-yaml.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { parseDocument } from 'yaml'; +import { updateSpecVersion } from './spec-updater.js'; + +const dir = mkdtempSync(join(tmpdir(), 'spec-updater-')); + +function write(name: string, content: string): string { + const p = join(dir, name); + writeFileSync(p, content, 'utf-8'); + return p; +} + +afterEach(() => vi.restoreAllMocks()); + +describe('updateSpecVersion - malformed YAML', () => { + // A multi-line single-quoted scalar with under-indented continuation lines produces + // recoverable parse errors: the document parses, but Document.toString() refuses to + // serialize it. The version field itself sits in a clean region of the file. + const malformed = `info: + title: Example SDK + version: 3.2.0 +paths: + /x: + get: + parameters: + - description: 'The agent extracts structured data from individual pages + on the site. Ideal for tracking pricing and catalog changes. + + ' + name: agent +`; + + it('does not throw, and syncs info.version in place despite parse errors', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const p = write('public.yaml', malformed); + + const changed = updateSpecVersion(p, '9.9.9', 'openapi'); + + expect(changed).toBe(true); + const after = readFileSync(p, 'utf-8'); + expect(parseDocument(after).getIn(['info', 'version'])).toBe('9.9.9'); + // malformed region is preserved byte-for-byte (only the version line differs) + expect(after).toContain('on the site. Ideal for tracking pricing'); + expect(warn).toHaveBeenCalledOnce(); + }); + + it('still works the normal way on a clean spec', () => { + const p = write('clean.yaml', 'info:\n title: X\n version: 1.0.0\n'); + const changed = updateSpecVersion(p, '2.0.0', 'openapi'); + expect(changed).toBe(true); + expect(parseDocument(readFileSync(p, 'utf-8')).getIn(['info', 'version'])).toBe('2.0.0'); + }); + + it('keeps a numeric-looking version a string (1.0 stays quoted-safe)', () => { + const p = write('numeric.yaml', 'info:\n title: X\n version: 3.0.0\n'); + updateSpecVersion(p, '1.0', 'openapi'); + expect(parseDocument(readFileSync(p, 'utf-8')).getIn(['info', 'version'])).toBe('1.0'); + }); +}); diff --git a/packages/changesets/versioning/spec-updater.ts b/packages/changesets/versioning/spec-updater.ts new file mode 100644 index 0000000..8cfb905 --- /dev/null +++ b/packages/changesets/versioning/spec-updater.ts @@ -0,0 +1,179 @@ +/** + * Spec Version Updater + * + * Updates the version field inside a spec file (e.g., info.version in OpenAPI) + * to keep the spec's internal version in sync with the tracked version. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { extname } from 'node:path'; +import { parseDocument } from 'yaml'; +import type { ContractType } from '@contractual/types'; + +/** + * Contract types that have a version field in the spec + */ +const VERSION_FIELD_PATHS: Partial> = { + openapi: ['info', 'version'], + asyncapi: ['info', 'version'], + odcs: ['version'], +}; + +/** + * Detect indentation used in a JSON file + */ +function detectJsonIndent(content: string): number { + const match = content.match(/^(\s+)"/m); + return match?.[1]?.length ?? 2; +} + +/** + * Ensure nested path exists in a plain object, creating intermediate objects as needed. + * Returns the parent object of the last key. + */ +function ensureJsonPath( + root: Record, + fieldPath: readonly string[] +): Record { + let current = root; + for (let i = 0; i < fieldPath.length - 1; i++) { + const key = fieldPath[i]!; + if (typeof current[key] !== 'object' || current[key] === null) { + current[key] = {}; + } + current = current[key] as Record; + } + return current; +} + +/** + * Update the version field inside a JSON spec file. + * Creates missing intermediate objects if needed. + */ +function updateJsonSpec(specPath: string, fieldPath: readonly string[], newVersion: string): void { + const content = readFileSync(specPath, 'utf-8'); + const spec = JSON.parse(content) as Record; + + const parent = ensureJsonPath(spec, fieldPath); + parent[fieldPath.at(-1)!] = newVersion; + + const indent = detectJsonIndent(content); + const trailingNewline = content.endsWith('\n') ? '\n' : ''; + writeFileSync(specPath, JSON.stringify(spec, null, indent) + trailingNewline, 'utf-8'); +} + +/** + * Render a string as a YAML scalar matching the quoting style of an existing node. + * Plain versions like `4.0.0` stay plain; quoted nodes keep their quote style so a + * value that would otherwise parse as a number (e.g. `1.0`) is preserved as a string. + */ +function renderScalar(value: string, originalType: string | undefined): string { + if (originalType === 'QUOTE_DOUBLE') { + return JSON.stringify(value); + } + if (originalType === 'QUOTE_SINGLE') { + return `'${value.replace(/'/g, "''")}'`; + } + // PLAIN or unknown: quote only if the value could be misread as a non-string scalar + return /^[\w.-]+$/.test(value) && !/^[+-]?(\d|\.\d|true|false|null|~)/i.test(value) + ? value + : JSON.stringify(value); +} + +/** + * Surgically replace just the target scalar's source range, leaving the rest of the + * file byte-for-byte intact. Used when the document has parse errors elsewhere and + * cannot be re-stringified as a whole. Returns true if the field was found and updated. + */ +function spliceScalarInPlace( + specPath: string, + content: string, + doc: ReturnType, + fieldPath: readonly string[], + newVersion: string +): boolean { + const node = doc.getIn([...fieldPath], true) as + | { range?: [number, number, number]; type?: string } + | undefined; + + // No range means the field lives in (or past) a malformed region we cannot edit safely. + if (!node?.range) { + return false; + } + + const [start, valueEnd] = node.range; + const replacement = renderScalar(newVersion, node.type); + const updated = content.slice(0, start) + replacement + content.slice(valueEnd); + writeFileSync(specPath, updated, 'utf-8'); + return true; +} + +/** + * Update the version field inside a YAML spec file. + * Uses parseDocument() to preserve comments, formatting, and key ordering. + * Creates missing intermediate keys if needed. + * + * If the document has recoverable parse errors elsewhere (e.g. a malformed + * description deep in the file), `Document.toString()` would refuse to serialize. + * In that case we fall back to a surgical in-place edit of the target scalar so a + * defect unrelated to the version field never takes down the release. If even that + * is not possible, we skip the sync with a clear warning rather than throwing. + */ +function updateYamlSpec(specPath: string, fieldPath: readonly string[], newVersion: string): void { + const content = readFileSync(specPath, 'utf-8'); + const doc = parseDocument(content); + + if (doc.errors.length === 0) { + // setIn creates intermediate nodes automatically + doc.setIn([...fieldPath], newVersion); + writeFileSync(specPath, doc.toString(), 'utf-8'); + return; + } + + // Document has parse errors; toString() would throw. Edit the field in place. + const first = doc.errors[0]!; + const where = first.linePos?.[0] ? ` (e.g. ${first.code} at line ${first.linePos[0].line})` : ''; + + if (spliceScalarInPlace(specPath, content, doc, fieldPath, newVersion)) { + console.warn( + `Spec "${specPath}" has YAML parse issues${where}; synced "${fieldPath.join('.')}" via in-place edit. Consider fixing the spec.` + ); + return; + } + + console.warn( + `Skipped version sync for "${specPath}": the file has YAML parse errors${where} and "${fieldPath.join('.')}" could not be located. The version field was not changed.` + ); +} + +/** + * Update the version field inside a spec file. + * + * Reads the spec, updates the appropriate version field based on contract type, + * and writes it back preserving the original format (YAML comments, JSON indentation). + * If the version field (or its parent, e.g. `info`) is missing, it will be created. + * + * @param specPath - Absolute path to the spec file + * @param newVersion - The new version string to set + * @param contractType - The type of contract (openapi, asyncapi, odcs, json-schema) + * @returns true if the file was updated, false if the spec type has no version field (e.g. json-schema) + */ +export function updateSpecVersion( + specPath: string, + newVersion: string, + contractType: ContractType +): boolean { + const fieldPath = VERSION_FIELD_PATHS[contractType]; + if (!fieldPath) { + return false; + } + + const ext = extname(specPath).toLowerCase(); + if (ext === '.json') { + updateJsonSpec(specPath, fieldPath, newVersion); + } else { + updateYamlSpec(specPath, fieldPath, newVersion); + } + + return true; +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..91d289b --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/cli@0.1.0-dev.8...@contractual/cli@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* bump @inquirer/prompts from 8.5.2 to 8.7.2 ([#21](https://github.com/codotech/contractual/issues/21)) ([e67033c](https://github.com/codotech/contractual/commit/e67033c1a9a635b3c68692db9c4fb3c7525492d1)), closes [#2255](https://github.com/codotech/contractual/issues/2255) [#1816](https://github.com/codotech/contractual/issues/1816) [#2254](https://github.com/codotech/contractual/issues/2254) [#2247](https://github.com/codotech/contractual/issues/2247) [#2244](https://github.com/codotech/contractual/issues/2244) [#2236](https://github.com/codotech/contractual/issues/2236) [#2251](https://github.com/codotech/contractual/issues/2251) [#2252](https://github.com/codotech/contractual/issues/2252) [#2249](https://github.com/codotech/contractual/issues/2249) [#2250](https://github.com/codotech/contractual/issues/2250) [#2248](https://github.com/codotech/contractual/issues/2248) +* bump chalk from 5.6.2 to 6.0.0 ([#22](https://github.com/codotech/contractual/issues/22)) ([bfc3d22](https://github.com/codotech/contractual/commit/bfc3d22503afd52fef470c80a6117b8a41688979)), closes [#689](https://github.com/codotech/contractual/issues/689) [#688](https://github.com/codotech/contractual/issues/688) [#687](https://github.com/codotech/contractual/issues/687) [#689](https://github.com/codotech/contractual/issues/689) [#688](https://github.com/codotech/contractual/issues/688) [#670](https://github.com/codotech/contractual/issues/670) [#687](https://github.com/codotech/contractual/issues/687) [#664](https://github.com/codotech/contractual/issues/664) +* bump ora from 8.2.0 to 9.4.1 ([#25](https://github.com/codotech/contractual/issues/25)) ([3eeb7e9](https://github.com/codotech/contractual/commit/3eeb7e951ef218f65eb2115058fe8c3f8e8184b6)), closes [#257](https://github.com/codotech/contractual/issues/257) [#251](https://github.com/codotech/contractual/issues/251) [#249](https://github.com/codotech/contractual/issues/249) [#257](https://github.com/codotech/contractual/issues/257) +* mitigate glob parsing and update vulnerable dependencies ([#32](https://github.com/codotech/contractual/issues/32)) ([45c00a0](https://github.com/codotech/contractual/commit/45c00a012d142df7da0ac1de626da58104fa5f1b)), closes [#31](https://github.com/codotech/contractual/issues/31) [#31](https://github.com/codotech/contractual/issues/31) [#31](https://github.com/codotech/contractual/issues/31) [#31](https://github.com/codotech/contractual/issues/31) +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/cli/bin/cli.js b/packages/cli/bin/cli.js index 924fff1..6021ca3 100755 --- a/packages/cli/bin/cli.js +++ b/packages/cli/bin/cli.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -import '../dist/index.js'; +import '../dist/commands.js'; diff --git a/packages/cli/package.json b/packages/cli/package.json index 38f1cf3..fb84b24 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,20 +1,28 @@ { "name": "@contractual/cli", "private": false, - "version": "0.0.0", + "version": "0.1.0-beta.0", "license": "MIT", "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "bin": { "contractual": "./bin/cli.js" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, "repository": { "type": "git", - "url": "https://github.com/contractual-dev/contractual.git", + "url": "https://github.com/codotech/contractual.git", "directory": "packages/cli" }, "homepage": "https://contractual.dev", "bugs": { - "url": "https://github.com/contractual-dev/contractual/issues" + "url": "https://github.com/codotech/contractual/issues" }, "contributors": [ { @@ -33,7 +41,7 @@ } ], "engines": { - "node": ">=18.12.0" + "node": ">=22.0.0" }, "scripts": { "prebuild": "pnpm rimraf dist", @@ -45,23 +53,28 @@ "files": [ "bin", "dist", + "src/config/schema.json", "README.md" ], "dependencies": { - "@contractual/generators.contract": "workspace:*", - "@contractual/generators.diff": "workspace:*", - "@contractual/generators.spec": "workspace:*", - "chalk": "^5.4.1", + "@contractual/changesets": "workspace:*", + "@contractual/governance": "workspace:*", + "@contractual/types": "workspace:*", + "@inquirer/prompts": "^8.7.2", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "chalk": "^6.0.0", "commander": "^12.1.0", - "inquirer": "^12.3.2", - "ora": "^8.1.1" + "fast-glob": "^3.3.3", + "ora": "^9.4.1", + "yaml": "^2.8.3" }, "publishConfig": { "access": "public", "provenance": true }, "devDependencies": { - "@vitest/coverage-c8": "^0.33.0", - "vitest": "^3.0.3" + "@vitest/coverage-v8": "^4.1.11", + "vitest": "^4.1.11" } } diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 1bff637..ae73ec5 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -1,22 +1,108 @@ import { Command } from 'commander'; -import { graduateSpec, generateContract } from './commands/generate.command.js'; +import { readFileSync } from 'node:fs'; +import { initCommand } from './commands/init.command.js'; +import { contractAddCommand, contractListCommand } from './commands/contract.command.js'; +import { lintCommand } from './commands/lint.command.js'; +import { diffCommand } from './commands/diff.command.js'; +import { breakingCommand } from './commands/breaking.command.js'; +import { changesetCommand } from './commands/changeset.command.js'; +import { versionCommand } from './commands/version.command.js'; +import { preEnterCommand, preExitCommand, preStatusCommand } from './commands/pre.command.js'; +import { statusCommand } from './commands/status.command.js'; const program = new Command(); -program.name('contractual'); - -const generateContractCommand = new Command('generate') - .description('Generate resources') - .command('contract') - .description('Generate a contract based on the provided OpenAPI file') - .action(() => { - return generateContract(); - }); - -const graduateSpecCommand = new Command('graduate').command('spec').action(() => { - return graduateSpec(); -}); -program.addCommand(graduateSpecCommand); -program.addCommand(generateContractCommand); +const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) + .version as string; +program + .name('contractual') + .description('Schema contract lifecycle orchestrator') + .version(packageVersion); + +program + .command('init') + .description('Initialize Contractual in this repository') + .option('-V, --initial-version ', 'Initial version for contracts') + .option('--versioning ', 'Versioning mode: independent, fixed') + .option('-y, --yes', 'Skip prompts and use defaults') + .option('--force', 'Reinitialize existing project') + .action(initCommand); + +const contractCmd = program.command('contract').description('Manage contracts'); + +contractCmd + .command('add') + .description('Add a new contract to the configuration') + .option('-n, --name ', 'Contract name') + .option('-t, --type ', 'Contract type: openapi, asyncapi, json-schema, odcs') + .option('-p, --path ', 'Path to spec file') + .option('--initial-version ', 'Initial version (default: 0.0.0)') + .option('--skip-validation', 'Skip spec validation') + .option('-y, --yes', 'Skip prompts and use defaults') + .action(contractAddCommand); + +contractCmd + .command('list [name]') + .description('List contracts (optionally filter by name)') + .option('--json', 'Output as JSON') + .action(contractListCommand); + +program + .command('lint') + .description('Lint all configured contracts') + .option('-c, --contract ', 'Lint specific contract') + .option('--format ', 'Output format: text, json', 'text') + .option('--fail-on-warn', 'Exit 1 on warnings') + .action(lintCommand); + +program + .command('diff') + .description('Show all changes between current specs and last versioned snapshots') + .option('-c, --contract ', 'Diff specific contract') + .option('--format ', 'Output format: text, json', 'text') + .option('--severity ', 'Filter: all, breaking, non-breaking, patch', 'all') + .option('--verbose', 'Show JSON Pointer paths for each change') + .action(diffCommand); + +program + .command('breaking') + .description('Detect breaking changes against last snapshot') + .option('-c, --contract ', 'Check specific contract') + .option('--format ', 'Output format: text, json', 'text') + .option('--fail-on ', 'Exit 1 on: breaking, non-breaking, any', 'breaking') + .action(breakingCommand); -program.parse(process.argv); +program + .command('changeset') + .description('Create changeset from detected changes') + .action(changesetCommand); + +program + .command('version') + .description('Consume changesets and bump versions') + .option('-y, --yes', 'Skip confirmation prompt') + .option('--dry-run', 'Preview without applying') + .option('--json', 'Output JSON (implies --yes)') + .option('--no-sync-version', 'Skip updating version field inside spec files') + .action(versionCommand); + +const preCmd = program.command('pre').description('Manage pre-release versions'); + +preCmd + .command('enter ') + .description('Enter pre-release mode (e.g., alpha, beta, rc)') + .action(preEnterCommand); + +preCmd.command('exit').description('Exit pre-release mode').action(preExitCommand); + +preCmd.command('status').description('Show pre-release status').action(preStatusCommand); + +program + .command('status') + .description('Show current versions and pending changesets') + .action(statusCommand); + +program.parseAsync(process.argv).catch((err) => { + console.error(`Error: ${err.message}`); + process.exit(2); +}); diff --git a/packages/cli/src/commands/breaking.command.ts b/packages/cli/src/commands/breaking.command.ts new file mode 100644 index 0000000..596e844 --- /dev/null +++ b/packages/cli/src/commands/breaking.command.ts @@ -0,0 +1,166 @@ +/** + * Breaking Command + * + * Detect breaking changes against snapshots. + * This is a CI gate β€” exits 1 if breaking changes are found. + * + * Uses the shared diffContracts() function internally. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { loadConfig } from '../config/index.js'; +import { diffContracts } from '../core/diff.js'; +import { formatSeverity } from '../utils/output.js'; +import type { DiffResult } from '@contractual/types'; + +interface BreakingOptions { + contract?: string; + format?: 'text' | 'json'; + failOn?: 'breaking' | 'non-breaking' | 'any'; +} + +interface BreakingCommandResult { + hasBreaking: boolean; + results: DiffResult[]; +} + +/** + * Detect breaking changes against snapshots + */ +export async function breakingCommand(options: BreakingOptions): Promise { + const spinner = ora('Loading configuration...').start(); + + let config; + try { + config = loadConfig(); + spinner.succeed('Configuration loaded'); + } catch (error) { + spinner.fail('Failed to load configuration'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red(message)); + process.exitCode = 1; + return; + } + + const checkSpinner = ora('Checking for breaking changes...').start(); + + try { + const { results } = await diffContracts(config, { + contracts: options.contract ? [options.contract] : undefined, + includeEmpty: true, + }); + + const hasBreaking = results.some((r) => r.summary.breaking > 0); + + if (hasBreaking) { + checkSpinner.fail('Breaking changes detected'); + } else { + checkSpinner.succeed('No breaking changes'); + } + + // Output results + console.log(); + if (options.format === 'json') { + const output: BreakingCommandResult = { hasBreaking, results }; + console.log(JSON.stringify(output, null, 2)); + } else { + printTextResults(results); + } + + // Determine exit code based on --fail-on option + const failOn = options.failOn ?? 'breaking'; + let shouldFail = false; + + if (failOn === 'any') { + // Fail on any detected changes + shouldFail = results.some((r) => r.changes.length > 0); + } else if (failOn === 'non-breaking') { + // Fail on non-breaking or breaking changes + shouldFail = results.some((r) => r.summary.breaking > 0 || r.summary.nonBreaking > 0); + } else { + // Default: fail only on breaking changes + shouldFail = hasBreaking; + } + + if (shouldFail) { + process.exitCode = 1; + } + } catch (error) { + checkSpinner.fail('Failed to check for breaking changes'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Error:'), message); + process.exitCode = 1; + } +} + +/** + * Print results in human-readable text format + */ +function printTextResults(results: DiffResult[]): void { + if (results.length === 0) { + console.log(chalk.gray('No contracts were checked.')); + return; + } + + for (const result of results) { + console.log(chalk.bold.underline(result.contract)); + console.log(); + + if (result.changes.length === 0) { + console.log(chalk.gray(' No changes detected.')); + console.log(); + continue; + } + + // Group changes by severity + const breaking = result.changes.filter((c) => c.severity === 'breaking'); + const nonBreaking = result.changes.filter((c) => c.severity === 'non-breaking'); + const patch = result.changes.filter((c) => c.severity === 'patch'); + const unknown = result.changes.filter((c) => c.severity === 'unknown'); + + // Print summary + console.log( + ` Summary: ` + + `${chalk.red(String(result.summary.breaking))} breaking, ` + + `${chalk.yellow(String(result.summary.nonBreaking))} non-breaking, ` + + `${chalk.green(String(result.summary.patch))} patch, ` + + `${chalk.gray(String(result.summary.unknown))} unknown` + ); + console.log(` Suggested bump: ${chalk.cyan(result.suggestedBump)}`); + console.log(); + + // Print changes by severity + if (breaking.length > 0) { + console.log(` ${formatSeverity('breaking')} Changes:`); + for (const change of breaking) { + console.log(` - ${change.path}: ${change.message}`); + } + console.log(); + } + + if (nonBreaking.length > 0) { + console.log(` ${formatSeverity('non-breaking')} Changes:`); + for (const change of nonBreaking) { + console.log(` - ${change.path}: ${change.message}`); + } + console.log(); + } + + if (patch.length > 0) { + console.log(` ${formatSeverity('patch')} Changes:`); + for (const change of patch) { + console.log(` - ${change.path}: ${change.message}`); + } + console.log(); + } + + if (unknown.length > 0) { + console.log(` ${formatSeverity('unknown')} Changes:`); + for (const change of unknown) { + console.log(` - ${change.path}: ${change.message}`); + } + console.log(); + } + } +} diff --git a/packages/cli/src/commands/changeset.command.ts b/packages/cli/src/commands/changeset.command.ts new file mode 100644 index 0000000..ba6a733 --- /dev/null +++ b/packages/cli/src/commands/changeset.command.ts @@ -0,0 +1,79 @@ +/** + * Changeset Command + * + * Auto-generate changeset from detected changes. + * Uses the shared diffContracts() function internally. + */ + +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import ora from 'ora'; +import { loadConfig } from '../config/index.js'; +import { diffContracts } from '../core/diff.js'; +import { CHANGESETS_DIR } from '../utils/files.js'; +import { + createChangeset, + generateUniqueChangesetName, + readChangesets, +} from '@contractual/changesets'; + +/** + * Auto-generate changeset from detected changes + */ +export async function changesetCommand(): Promise { + const spinner = ora('Loading configuration...').start(); + + let config; + try { + config = loadConfig(); + spinner.succeed('Configuration loaded'); + } catch (error) { + spinner.fail('Failed to load configuration'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red(message)); + process.exit(1); + } + + // Detect changes for all contracts using shared diff logic + const diffSpinner = ora('Detecting changes...').start(); + + try { + const { results: diffResults, contractualDir } = await diffContracts(config, { + includeEmpty: false, // Only get contracts with actual changes + }); + + if (diffResults.length === 0) { + diffSpinner.succeed('No changes detected'); + console.log(chalk.gray('No changeset created.')); + process.exit(0); + } + + diffSpinner.succeed(`Detected changes in ${diffResults.length} contract(s)`); + + // Create changeset content + const { content: changesetContent } = createChangeset(diffResults); + + // Read existing changesets to ensure unique name + const changesetsDir = join(contractualDir, CHANGESETS_DIR); + const existingChangesets = await readChangesets(changesetsDir); + const existingNames = existingChangesets.map((c) => c.filename.replace(/\.md$/, '')); + const changesetName = generateUniqueChangesetName(existingNames); + + // Write changeset file + const changesetPath = join(changesetsDir, `${changesetName}.md`); + + writeFileSync(changesetPath, changesetContent, 'utf-8'); + + console.log(); + console.log(chalk.green('Created changeset:'), chalk.cyan(changesetPath)); + console.log(); + console.log(chalk.gray('You can edit this file to add more details about the changes.')); + console.log(chalk.gray('Run `contractual version` to consume changesets and bump versions.')); + } catch (error) { + diffSpinner.fail('Failed to detect changes'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Error:'), message); + process.exit(1); + } +} diff --git a/packages/cli/src/commands/contract.command.ts b/packages/cli/src/commands/contract.command.ts new file mode 100644 index 0000000..c62b9a9 --- /dev/null +++ b/packages/cli/src/commands/contract.command.ts @@ -0,0 +1,374 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve, extname } from 'node:path'; +import chalk from 'chalk'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; +import { VersionManager, updateSpecVersion } from '@contractual/changesets'; +import { loadConfig } from '../config/index.js'; +import { + ensureContractualDir, + detectSpecType, + CONTRACTUAL_DIR, + findContractualDir, +} from '../utils/files.js'; +import { + promptInput, + promptSelect, + promptVersion, + CONTRACT_TYPE_CHOICES, + type PromptOptions, +} from '../utils/prompts.js'; +import type { ContractDefinition, ContractType } from '@contractual/types'; + +/** + * Default version for new contracts + */ +const DEFAULT_VERSION = '0.0.0'; + +/** + * Options for the contract add command + */ +interface ContractAddOptions extends PromptOptions { + /** Contract name */ + name?: string; + /** Contract type */ + type?: ContractType; + /** Path to spec file */ + path?: string; + /** Initial version */ + initialVersion?: string; + /** Skip validation */ + skipValidation?: boolean; +} + +/** + * Add a new contract to the configuration + */ +export async function contractAddCommand(options: ContractAddOptions = {}): Promise { + const cwd = process.cwd(); + const configPath = join(cwd, 'contractual.yaml'); + + // Check if initialized + if (!existsSync(configPath)) { + console.log(chalk.red('Not initialized:') + ' contractual.yaml not found'); + console.log(chalk.dim('Run `contractual init` first')); + process.exitCode = 1; + return; + } + + // Read existing config + const configContent = readFileSync(configPath, 'utf-8'); + const config = parseYaml(configContent) as { + contracts?: ContractDefinition[]; + changeset?: unknown; + versioning?: unknown; + ai?: unknown; + }; + + if (!config.contracts) { + config.contracts = []; + } + + // Get contract details through prompts or options + const contractName = await getContractName(config.contracts, options); + if (!contractName) return; + + const specPath = await getSpecPath(cwd, options); + if (!specPath) return; + + const contractType = await getContractType(cwd, specPath, options); + if (!contractType) return; + + const version = await getVersion(options); + + // Validate spec file + if (!options.skipValidation) { + const absolutePath = resolve(cwd, specPath); + const detectedType = detectSpecType(absolutePath); + + if (!detectedType) { + console.log(chalk.red('Invalid spec file:') + ' Could not detect spec type'); + console.log(chalk.dim(`Expected: ${contractType}`)); + process.exitCode = 1; + return; + } + + if (detectedType !== contractType) { + console.log( + chalk.yellow('Type mismatch:') + + ` Detected ${chalk.cyan(detectedType)}, specified ${chalk.cyan(contractType)}` + ); + console.log(chalk.dim('Use --skip-validation to override')); + process.exitCode = 1; + return; + } + + console.log(chalk.green('βœ“') + ` Valid ${contractType} spec`); + } + + // Create contract definition + const contract: ContractDefinition = { + name: contractName, + type: contractType, + path: specPath, + }; + + // Add to config + config.contracts.push(contract); + + // Write updated config + const yamlContent = stringifyYaml(config, { + lineWidth: 100, + singleQuote: true, + }); + writeFileSync(configPath, yamlContent, 'utf-8'); + + // Ensure .contractual directory exists and create snapshot + const contractualDir = findContractualDir(cwd) ?? join(cwd, CONTRACTUAL_DIR); + ensureContractualDir(cwd); + + const versionManager = new VersionManager(contractualDir); + const absolutePath = resolve(cwd, specPath); + updateSpecVersion(absolutePath, version, contractType); + versionManager.setVersion(contractName, version, absolutePath); + + // Print summary + const snapshotExt = extname(specPath) || '.yaml'; + console.log(); + console.log( + chalk.green('βœ“') + ` Added ${chalk.cyan(contractName)} (${contractType}) at v${version}` + ); + console.log(); + console.log(chalk.bold('Updated:')); + console.log(` ${chalk.yellow('~')} contractual.yaml`); + console.log(chalk.bold('Created:')); + console.log(` ${chalk.green('+')} .contractual/snapshots/${contractName}${snapshotExt}`); + console.log(` ${chalk.green('+')} .contractual/versions.json (updated)`); +} + +/** + * Get contract name through prompts or options + */ +async function getContractName( + existingContracts: ContractDefinition[], + options: ContractAddOptions +): Promise { + const existingNames = new Set(existingContracts.map((c) => c.name)); + + if (options.name) { + if (existingNames.has(options.name)) { + console.log(chalk.red('Contract exists:') + ` ${options.name} already defined`); + process.exitCode = 1; + return null; + } + return options.name; + } + + const name = await promptInput('Contract name:', '', options); + + if (!name) { + console.log(chalk.red('Contract name is required')); + process.exitCode = 1; + return null; + } + + if (existingNames.has(name)) { + console.log(chalk.red('Contract exists:') + ` ${name} already defined`); + process.exitCode = 1; + return null; + } + + // Validate name format (alphanumeric, hyphens, underscores) + if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) { + console.log( + chalk.red('Invalid name:') + + ' Must start with letter, contain only letters, numbers, hyphens, underscores' + ); + process.exitCode = 1; + return null; + } + + return name; +} + +/** + * Get spec path through prompts or options + */ +async function getSpecPath(cwd: string, options: ContractAddOptions): Promise { + if (options.path) { + const absolutePath = resolve(cwd, options.path); + if (!existsSync(absolutePath)) { + console.log(chalk.red('File not found:') + ` ${options.path}`); + process.exitCode = 1; + return null; + } + return options.path; + } + + const path = await promptInput('Path to spec file:', '', options); + + if (!path) { + console.log(chalk.red('Spec path is required')); + process.exitCode = 1; + return null; + } + + const absolutePath = resolve(cwd, path); + if (!existsSync(absolutePath)) { + console.log(chalk.red('File not found:') + ` ${path}`); + process.exitCode = 1; + return null; + } + + return path; +} + +/** + * Get contract type through prompts or options + */ +async function getContractType( + cwd: string, + specPath: string, + options: ContractAddOptions +): Promise { + if (options.type) { + return options.type; + } + + // Try to auto-detect type + const absolutePath = resolve(cwd, specPath); + const detectedType = detectSpecType(absolutePath); + + if (detectedType && options.yes) { + return detectedType; + } + + const typeChoices = CONTRACT_TYPE_CHOICES.map((c) => ({ + ...c, + name: detectedType === c.value ? `${c.name} (detected)` : c.name, + })); + + return promptSelect('Contract type:', [...typeChoices], detectedType ?? 'openapi', options); +} + +/** + * Get version through prompts or options + */ +async function getVersion(options: ContractAddOptions): Promise { + if (options.initialVersion) { + return options.initialVersion; + } + + return promptVersion('Initial version:', DEFAULT_VERSION, options); +} + +/** + * Options for the contract list command + */ +interface ContractListOptions { + /** Output as JSON */ + json?: boolean; +} + +/** + * Contract info for list output + */ +interface ContractInfo { + name: string; + type: ContractType; + version: string; + path: string; +} + +/** + * List contracts + */ +export async function contractListCommand( + name: string | undefined, + options: ContractListOptions = {} +): Promise { + let config; + try { + config = loadConfig(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Failed to load configuration:'), message); + process.exitCode = 1; + return; + } + + const contractualDir = findContractualDir(config.configDir); + const versionManager = contractualDir ? new VersionManager(contractualDir) : null; + + // Build contract info list + let contracts: ContractInfo[] = config.contracts.map((c) => ({ + name: c.name, + type: c.type, + version: versionManager?.getVersion(c.name) ?? '0.0.0', + path: c.path, + })); + + // Filter by name if provided + if (name) { + contracts = contracts.filter((c) => c.name === name); + if (contracts.length === 0) { + console.error(chalk.red(`Contract not found: ${name}`)); + process.exitCode = 1; + return; + } + } + + // Output + if (options.json) { + console.log(JSON.stringify(contracts, null, 2)); + return; + } + + // Table output + if (contracts.length === 0) { + console.log(chalk.dim('No contracts configured.')); + return; + } + + // Calculate column widths + const maxNameLen = Math.max(4, ...contracts.map((c) => c.name.length)); + const maxTypeLen = Math.max(4, ...contracts.map((c) => c.type.length)); + const maxVersionLen = Math.max(7, ...contracts.map((c) => c.version.length)); + + // Header + const header = + `${'Name'.padEnd(maxNameLen)} ` + + `${'Type'.padEnd(maxTypeLen)} ` + + `${'Version'.padEnd(maxVersionLen)} ` + + `Path`; + console.log(chalk.dim(header)); + console.log(chalk.dim('─'.repeat(header.length + 10))); + + // Rows + for (const contract of contracts) { + const typeColor = getTypeColor(contract.type); + console.log( + `${chalk.cyan(contract.name.padEnd(maxNameLen))} ` + + `${typeColor(contract.type.padEnd(maxTypeLen))} ` + + `${chalk.green(contract.version.padEnd(maxVersionLen))} ` + + `${chalk.dim(contract.path)}` + ); + } +} + +/** + * Get chalk color function for contract type + */ +function getTypeColor(type: ContractType): (text: string) => string { + switch (type) { + case 'openapi': + return chalk.green; + case 'asyncapi': + return chalk.magenta; + case 'json-schema': + return chalk.blue; + case 'odcs': + return chalk.yellow; + default: + return chalk.white; + } +} diff --git a/packages/cli/src/commands/diff.command.ts b/packages/cli/src/commands/diff.command.ts new file mode 100644 index 0000000..e006aed --- /dev/null +++ b/packages/cli/src/commands/diff.command.ts @@ -0,0 +1,71 @@ +/** + * Diff Command + * + * Show all changes between current specs and their last versioned snapshots, + * classified by severity. + * + * Unlike `breaking`, which is a CI gate (exits 1 on breaking changes), + * `diff` is informational β€” it always exits 0 on success. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { loadConfig } from '../config/index.js'; +import { diffContracts } from '../core/diff.js'; +import { formatDiffText, formatDiffJson, filterBySeverity } from '../formatters/diff.js'; + +interface DiffOptions { + contract?: string; + format?: 'text' | 'json'; + severity?: 'all' | 'breaking' | 'non-breaking' | 'patch'; + verbose?: boolean; +} + +/** + * Show all changes between current specs and last versioned snapshots + */ +export async function diffCommand(options: DiffOptions): Promise { + const spinner = ora('Loading configuration...').start(); + + let config; + try { + config = loadConfig(); + spinner.succeed('Configuration loaded'); + } catch (error) { + spinner.fail('Failed to load configuration'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red(message)); + process.exitCode = 2; + return; + } + + const diffSpinner = ora('Comparing specs against snapshots...').start(); + + try { + const { results } = await diffContracts(config, { + contracts: options.contract ? [options.contract] : undefined, + includeEmpty: true, // Show "no changes" for contracts with no diff + }); + + diffSpinner.succeed('Comparison complete'); + console.log(); + + // Apply severity filter + const filtered = filterBySeverity(results, options.severity ?? 'all'); + + // Output results + if (options.format === 'json') { + console.log(formatDiffJson(filtered)); + } else { + formatDiffText(filtered, { verbose: options.verbose }); + } + + // diff always exits 0 on success (it's informational, not a gate) + process.exitCode = 0; + } catch (error) { + diffSpinner.fail('Comparison failed'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Error:'), message); + process.exitCode = 3; // Tool execution error + } +} diff --git a/packages/cli/src/commands/generate.command.ts b/packages/cli/src/commands/generate.command.ts deleted file mode 100644 index 01d1e2c..0000000 --- a/packages/cli/src/commands/generate.command.ts +++ /dev/null @@ -1,29 +0,0 @@ -import inquirer from 'inquirer'; -import { createContractCommandHandler } from '@contractual/generators.contract'; -import ora from 'ora'; -import chalk from 'chalk'; -import path from 'node:path'; -import process from 'node:process'; -import { - generateSpecification, - getLatestVersion, - initializePaths, -} from '@contractual/generators.spec'; - -export function generateContract() { - return createContractCommandHandler( - ora, - chalk, - console, - path.resolve( - process.cwd(), - 'contractual', - 'specs', - `openapi-v${getLatestVersion(initializePaths().configFilePath)}.yaml` - ) - ).handle(); -} - -export function graduateSpec() { - return generateSpecification(inquirer); -} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 0000000..1263f3e --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,6 @@ +export * from './init.command.js'; +export * from './lint.command.js'; +export * from './breaking.command.js'; +export * from './changeset.command.js'; +export * from './version.command.js'; +export * from './status.command.js'; diff --git a/packages/cli/src/commands/init.command.ts b/packages/cli/src/commands/init.command.ts new file mode 100644 index 0000000..376ba3e --- /dev/null +++ b/packages/cli/src/commands/init.command.ts @@ -0,0 +1,397 @@ +import { existsSync, writeFileSync, readFileSync } from 'node:fs'; +import { join, basename } from 'node:path'; +import fg from 'fast-glob'; +import chalk from 'chalk'; +import ora from 'ora'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; +import { VersionManager } from '@contractual/changesets'; +import { + ensureContractualDir, + detectSpecType, + CONTRACTUAL_DIR, + getSnapshotPath, +} from '../utils/files.js'; +import { + promptSelect, + promptVersion, + promptConfirm, + VERSION_CHOICES, + VERSIONING_MODE_CHOICES, + type PromptOptions, +} from '../utils/prompts.js'; +import type { ContractDefinition, ContractType, VersioningMode } from '@contractual/types'; + +/** + * Default starting version for new contracts + */ +const DEFAULT_VERSION = '0.0.0'; + +/** + * Default versioning mode + */ +const DEFAULT_VERSIONING_MODE: VersioningMode = 'independent'; + +/** + * Options for the init command + */ +interface InitOptions extends PromptOptions { + /** Initial version for contracts */ + initialVersion?: string; + /** Versioning mode */ + versioning?: VersioningMode; + /** Force reinitialize */ + force?: boolean; +} + +/** + * Glob patterns to find spec files + */ +const SPEC_PATTERNS = [ + '**/*.openapi.yaml', + '**/*.openapi.yml', + '**/*.openapi.json', + '**/openapi.yaml', + '**/openapi.yml', + '**/openapi.json', + '**/*.asyncapi.yaml', + '**/*.asyncapi.yml', + '**/*.asyncapi.json', + '**/*.schema.json', + '**/*.odcs.yaml', + '**/*.odcs.yml', +]; + +/** + * Directories to ignore when scanning + */ +const IGNORE_PATTERNS = [ + '**/node_modules/**', + '**/.git/**', + '**/dist/**', + '**/build/**', + '**/.contractual/**', +]; + +/** + * Extract contract name from file path + * Uses the filename stem without extension patterns + */ +function extractContractName(filePath: string): string { + const base = basename(filePath); + + // Remove known suffixes and extensions + let name = base + .replace(/\.openapi\.(ya?ml|json)$/i, '') + .replace(/\.asyncapi\.(ya?ml|json)$/i, '') + .replace(/\.schema\.json$/i, '') + .replace(/\.odcs\.ya?ml$/i, '') + .replace(/\.(ya?ml|json)$/i, ''); + + // Handle generic names like "openapi" -> use parent directory name + if (['openapi', 'asyncapi', 'schema', 'spec', 'api'].includes(name.toLowerCase())) { + const parts = filePath.split('/'); + if (parts.length >= 2) { + name = parts[parts.length - 2]; + } + } + + return name; +} + +/** + * Get initial version through prompts or options + */ +async function getInitialVersion(options: InitOptions): Promise { + // If version provided via CLI, use it + if (options.initialVersion) { + return options.initialVersion; + } + + // Prompt for version + const versionChoice = await promptSelect( + 'Initial version for contracts:', + [...VERSION_CHOICES], + '0.0.0', + options + ); + + if (versionChoice === 'custom') { + return promptVersion('Enter version:', DEFAULT_VERSION, options); + } + + return versionChoice; +} + +/** + * Get versioning mode through prompts or options + */ +async function getVersioningMode(options: InitOptions): Promise { + if (options.versioning) { + return options.versioning; + } + + return promptSelect( + 'Versioning mode:', + [...VERSIONING_MODE_CHOICES], + DEFAULT_VERSIONING_MODE, + options + ); +} + +/** + * Initialize Contractual in a repository + * + * Scans for spec files and generates contractual.yaml configuration + */ +export async function initCommand(options: InitOptions = {}): Promise { + const cwd = process.cwd(); + const configPath = join(cwd, 'contractual.yaml'); + const contractualDir = join(cwd, CONTRACTUAL_DIR); + + // Check if already initialized + if (existsSync(configPath) && !options.force) { + // Try to handle existing project with uninitialized contracts + await handleExistingProject(cwd, configPath, contractualDir, options); + return; + } + + const spinner = ora('Scanning for spec files...').start(); + + try { + // Scan for spec files + const files = await fg(SPEC_PATTERNS, { + cwd, + ignore: IGNORE_PATTERNS, + absolute: false, + onlyFiles: true, + }); + + spinner.succeed(`Found ${files.length} potential spec file(s)`); + + // Build contract definitions + const contracts: ContractDefinition[] = []; + const seenNames = new Set(); + + for (const filePath of files) { + const absolutePath = join(cwd, filePath); + const detectedType = detectSpecType(absolutePath); + + if (!detectedType) { + continue; + } + + let name = extractContractName(filePath); + + // Ensure unique names + if (seenNames.has(name)) { + let counter = 2; + while (seenNames.has(`${name}-${counter}`)) { + counter++; + } + name = `${name}-${counter}`; + } + seenNames.add(name); + + contracts.push({ + name, + type: detectedType, + path: filePath, + }); + } + + if (contracts.length === 0) { + console.log(chalk.yellow('\nNo spec files found')); + console.log(chalk.dim('\nSupported file patterns:')); + console.log(chalk.dim(' - *.openapi.yaml/json')); + console.log(chalk.dim(' - *.asyncapi.yaml/json')); + console.log(chalk.dim(' - *.schema.json')); + console.log(chalk.dim(' - *.odcs.yaml')); + console.log(chalk.dim('\nYou can manually create contractual.yaml to define contracts.')); + return; + } + + // Show found contracts + console.log(); + for (const contract of contracts) { + const typeColor = getTypeColor(contract.type); + console.log( + ` ${chalk.dim('Found:')} ${contract.path} ${chalk.dim('(')}${typeColor(contract.type)}${chalk.dim(')')}` + ); + } + console.log(); + + // Get version and mode through prompts + const initialVersion = await getInitialVersion(options); + const versioningMode = await getVersioningMode(options); + + // Generate config + const config: Record = { + contracts, + changeset: { + autoDetect: true, + requireOnPR: true, + }, + }; + + // Only add versioning section if not using defaults + if (versioningMode !== 'independent') { + config.versioning = { + mode: versioningMode, + }; + } + + // Write contractual.yaml + const yamlContent = stringifyYaml(config, { + lineWidth: 100, + singleQuote: true, + }); + writeFileSync(configPath, yamlContent, 'utf-8'); + + // Create .contractual directory structure + const createdDir = ensureContractualDir(cwd); + + // Create snapshots and set initial versions + const versionManager = new VersionManager(createdDir); + for (const contract of contracts) { + const absolutePath = join(cwd, contract.path); + versionManager.setVersion(contract.name, initialVersion, absolutePath); + } + + // Print summary + console.log(); + console.log(chalk.green('βœ“') + ' Initialized Contractual'); + console.log(); + console.log(chalk.bold('Created:')); + console.log(` ${chalk.green('+')} contractual.yaml`); + console.log(` ${chalk.green('+')} .contractual/`); + console.log(` ${chalk.green('+')} .contractual/changesets/`); + console.log(` ${chalk.green('+')} .contractual/snapshots/`); + console.log(` ${chalk.green('+')} .contractual/versions.json`); + + console.log(); + console.log(chalk.bold(`Detected ${contracts.length} contract(s) at v${initialVersion}:`)); + + for (const contract of contracts) { + const typeColor = getTypeColor(contract.type); + console.log( + ` ${chalk.cyan(contract.name)} ${chalk.dim('(')}${typeColor(contract.type)}${chalk.dim(')')}` + ); + console.log(` ${chalk.dim(contract.path)}`); + } + + if (versioningMode !== 'independent') { + console.log(); + console.log(chalk.dim(`Versioning mode: ${versioningMode}`)); + } + + console.log(); + console.log(chalk.dim('Next steps:')); + console.log(chalk.dim(' 1. Run `contractual lint` to validate your specs')); + console.log(chalk.dim(' 2. Make changes to your specs')); + console.log(chalk.dim(' 3. Run `contractual diff` to see changes')); + console.log(chalk.dim(' 4. Run `contractual changeset` to record changes')); + } catch (error) { + spinner.fail('Initialization failed'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red(message)); + process.exitCode = 1; + } +} + +/** + * Handle existing project - initialize uninitialized contracts + */ +async function handleExistingProject( + cwd: string, + configPath: string, + contractualDir: string, + options: InitOptions +): Promise { + // Read existing config + const configContent = readFileSync(configPath, 'utf-8'); + const config = parseYaml(configContent) as { contracts?: ContractDefinition[] }; + + if (!config.contracts || config.contracts.length === 0) { + console.log(chalk.red('Already initialized:') + ' contractual.yaml exists'); + console.log(chalk.dim('Use `contractual status` to see current state')); + process.exitCode = 1; + return; + } + + // Ensure .contractual directory exists + ensureContractualDir(cwd); + + // Find contracts without snapshots + const uninitializedContracts: ContractDefinition[] = []; + for (const contract of config.contracts) { + const snapshotPath = getSnapshotPath(contract.name, contractualDir); + if (!snapshotPath) { + uninitializedContracts.push(contract); + } + } + + if (uninitializedContracts.length === 0) { + console.log(chalk.yellow('Already initialized:') + ' contractual.yaml exists'); + console.log(chalk.dim('All contracts have snapshots.')); + console.log(chalk.dim('Use `contractual status` to see current state')); + console.log(chalk.dim('Use `--force` to reinitialize')); + return; + } + + // Show uninitialized contracts + console.log( + chalk.yellow(`Found ${uninitializedContracts.length} contract(s) without version history:`) + ); + for (const contract of uninitializedContracts) { + console.log( + ` ${chalk.dim('-')} ${chalk.cyan(contract.name)} ${chalk.dim(`(${contract.type})`)}` + ); + } + console.log(); + + // Confirm initialization + const shouldInitialize = await promptConfirm( + `Initialize with version ${DEFAULT_VERSION}?`, + true, + options + ); + + if (!shouldInitialize) { + console.log(chalk.dim('Skipped initialization')); + return; + } + + // Initialize uninitialized contracts + const versionManager = new VersionManager(contractualDir); + for (const contract of uninitializedContracts) { + const absolutePath = join(cwd, contract.path); + if (!existsSync(absolutePath)) { + console.log( + chalk.yellow(` Skipped ${contract.name}: spec file not found at ${contract.path}`) + ); + continue; + } + versionManager.setVersion(contract.name, DEFAULT_VERSION, absolutePath); + console.log( + chalk.green('βœ“') + ` Initialized ${chalk.cyan(contract.name)} at v${DEFAULT_VERSION}` + ); + } +} + +/** + * Get chalk color function for contract type + */ +function getTypeColor(type: ContractType): (text: string) => string { + switch (type) { + case 'openapi': + return chalk.green; + case 'asyncapi': + return chalk.magenta; + case 'json-schema': + return chalk.blue; + case 'odcs': + return chalk.yellow; + default: + return chalk.white; + } +} diff --git a/packages/cli/src/commands/lint.command.ts b/packages/cli/src/commands/lint.command.ts new file mode 100644 index 0000000..dda57d2 --- /dev/null +++ b/packages/cli/src/commands/lint.command.ts @@ -0,0 +1,236 @@ +import chalk from 'chalk'; +import ora from 'ora'; +import { loadConfig } from '../config/index.js'; +import { getLinter as getRegisteredLinter } from '../governance/index.js'; +import { printSuccess, printError, printWarning } from '../utils/output.js'; +import type { LintResult, LintFn, ResolvedContract } from '@contractual/types'; + +/** + * Options for the lint command + */ +export interface LintOptions { + /** Filter to specific contract name */ + contract?: string; + /** Output format */ + format?: 'text' | 'json'; + /** Exit 1 on warnings */ + failOnWarn?: boolean; +} + +/** + * Result type for linter lookup + */ +type LinterLookupResult = + | { status: 'disabled' } + | { status: 'not-found'; type: string } + | { status: 'found'; linter: LintFn }; + +/** + * Get linter for a contract using the governance registry + */ +function getLinterForContract(contract: ResolvedContract): LinterLookupResult { + // Check if linting is explicitly disabled + if (contract.lint === false) { + return { status: 'disabled' }; + } + + // Use the governance registry to get the linter + const linter = getRegisteredLinter(contract.type, contract.lint); + + if (linter === null) { + return { status: 'not-found', type: contract.type }; + } + + if (!linter) { + return { status: 'not-found', type: contract.type }; + } + + return { status: 'found', linter }; +} + +/** + * Run linters for all configured contracts + */ +export async function lintCommand(options: LintOptions = {}): Promise { + const { contract: filterContract, format = 'text', failOnWarn = false } = options; + + try { + // Load config + const config = loadConfig(); + + // Filter contracts if specified + let contracts = config.contracts; + if (filterContract) { + contracts = contracts.filter((c) => c.name === filterContract); + if (contracts.length === 0) { + if (format === 'json') { + console.log(JSON.stringify({ error: `Contract "${filterContract}" not found` })); + } else { + printError(`Contract "${filterContract}" not found`); + } + process.exitCode = 1; + return; + } + } + + if (contracts.length === 0) { + if (format === 'json') { + console.log(JSON.stringify({ results: [], errors: 0, warnings: 0 })); + } else { + printWarning('No contracts configured'); + } + return; + } + + const results: LintResult[] = []; + const spinner = format === 'text' ? ora('Linting contracts...').start() : null; + + for (const contract of contracts) { + if (spinner) { + spinner.text = `Linting ${contract.name}...`; + } + + const linterResult = getLinterForContract(contract); + + // Linting explicitly disabled + if (linterResult.status === 'disabled') { + if (format === 'text') { + spinner?.stopAndPersist({ + symbol: chalk.dim('-'), + text: `${contract.name}: ${chalk.dim('linting disabled')}`, + }); + spinner?.start(); + } + continue; + } + + // No linter registered for this type + if (linterResult.status === 'not-found') { + results.push({ + contract: contract.name, + specPath: contract.absolutePath, + errors: [ + { + path: '', + severity: 'error', + message: `No linter available for ${contract.type}. Configure a custom lint command or set lint: false.`, + }, + ], + warnings: [], + }); + if (format === 'text') { + spinner?.stopAndPersist({ + symbol: chalk.yellow('!'), + text: `${contract.name}: ${chalk.yellow(`no linter available for ${linterResult.type}`)}`, + }); + spinner?.start(); + } + continue; + } + + try { + const result = await linterResult.linter(contract.absolutePath); + results.push({ + ...result, + contract: contract.name, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Linter execution failed'; + results.push({ + contract: contract.name, + specPath: contract.absolutePath, + errors: [ + { + path: '', + message, + severity: 'error', + }, + ], + warnings: [], + }); + } + } + + spinner?.stop(); + + // Calculate totals + const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0); + const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0); + + // Output results + if (format === 'json') { + console.log( + JSON.stringify( + { + results, + errors: totalErrors, + warnings: totalWarnings, + }, + null, + 2 + ) + ); + } else { + // Text format output + console.log(); + + for (const result of results) { + const hasErrors = result.errors.length > 0; + const hasWarnings = result.warnings.length > 0; + + if (!hasErrors && !hasWarnings) { + printSuccess(`${result.contract}: No issues found`); + continue; + } + + // Print contract header + const errorCount = result.errors.length; + const warningCount = result.warnings.length; + const summary = []; + if (errorCount > 0) summary.push(chalk.red(`${errorCount} error(s)`)); + if (warningCount > 0) summary.push(chalk.yellow(`${warningCount} warning(s)`)); + + console.log(`${chalk.bold(result.contract)}: ${summary.join(', ')}`); + + // Print errors + for (const error of result.errors) { + const location = error.path ? chalk.dim(`[${error.path}]`) : ''; + const rule = error.rule ? chalk.dim(`(${error.rule})`) : ''; + console.log(` ${chalk.red('error')} ${location} ${error.message} ${rule}`); + } + + // Print warnings + for (const warning of result.warnings) { + const location = warning.path ? chalk.dim(`[${warning.path}]`) : ''; + const rule = warning.rule ? chalk.dim(`(${warning.rule})`) : ''; + console.log(` ${chalk.yellow('warn')} ${location} ${warning.message} ${rule}`); + } + + console.log(); + } + + // Summary + if (results.length > 0) { + console.log( + chalk.dim( + `Checked ${results.length} contract(s): ` + + `${totalErrors} error(s), ${totalWarnings} warning(s)` + ) + ); + } + } + + // Exit with error code if there were errors (or warnings if --fail-on-warn) + if (totalErrors > 0 || (failOnWarn && totalWarnings > 0)) { + process.exitCode = 1; + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + if (options.format === 'json') { + console.log(JSON.stringify({ error: message })); + } else { + printError(message); + } + process.exitCode = 1; + } +} diff --git a/packages/cli/src/commands/pre.command.ts b/packages/cli/src/commands/pre.command.ts new file mode 100644 index 0000000..e962aa6 --- /dev/null +++ b/packages/cli/src/commands/pre.command.ts @@ -0,0 +1,172 @@ +import chalk from 'chalk'; +import { VersionManager, PreReleaseManager } from '@contractual/changesets'; +import { findContractualDir } from '../utils/files.js'; + +/** + * Enter pre-release mode + * @param tag - The pre-release tag (e.g., "alpha", "beta", "rc") + */ +export async function preEnterCommand(tag: string): Promise { + const cwd = process.cwd(); + const contractualDir = findContractualDir(cwd); + + if (!contractualDir) { + console.error(chalk.red('No .contractual directory found. Run `contractual init` first.')); + process.exitCode = 1; + return; + } + + try { + const versionManager = new VersionManager(contractualDir); + const preManager = new PreReleaseManager(contractualDir); + + if (preManager.isActive()) { + const state = preManager.getState(); + console.log(chalk.yellow('Already in pre-release mode:') + ` ${state?.tag}`); + console.log(chalk.dim('Run `contractual pre exit` to leave pre-release mode first.')); + process.exitCode = 1; + return; + } + + preManager.enter(tag, versionManager); + + console.log(chalk.green('βœ“') + ` Entered pre-release mode: ${chalk.cyan(tag)}`); + console.log(); + console.log(chalk.bold('Created:')); + console.log(` ${chalk.green('+')} .contractual/pre.json`); + console.log(); + console.log(chalk.dim(`Next versions will use ${tag} identifier (e.g., 2.0.0-${tag}.0)`)); + console.log( + chalk.dim('Run `contractual version` to apply changesets with pre-release versions.') + ); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Failed to enter pre-release mode:'), message); + process.exitCode = 1; + } +} + +/** + * Exit pre-release mode + */ +export async function preExitCommand(): Promise { + const cwd = process.cwd(); + const contractualDir = findContractualDir(cwd); + + if (!contractualDir) { + console.error(chalk.red('No .contractual directory found. Run `contractual init` first.')); + process.exitCode = 1; + return; + } + + try { + const versionManager = new VersionManager(contractualDir); + const preManager = new PreReleaseManager(contractualDir); + + if (!preManager.isActive()) { + console.log(chalk.yellow('Not in pre-release mode.')); + return; + } + + const state = preManager.getState(); + const currentVersions = versionManager.getAllVersions(); + + // Show what will change + console.log(chalk.bold('Exiting pre-release mode')); + console.log(); + + const hasPreReleaseVersions = Object.entries(currentVersions).some(([_, version]) => + version.includes('-') + ); + + if (hasPreReleaseVersions) { + console.log(chalk.dim('Current pre-release versions:')); + for (const [contract, version] of Object.entries(currentVersions)) { + if (version.includes('-')) { + // Extract base version + const baseVersion = version.split('-')[0]; + console.log( + ` ${chalk.cyan(contract)}: ${chalk.gray(version)} β†’ ${chalk.green(baseVersion)}` + ); + } + } + console.log(); + console.log(chalk.dim('Run `contractual version` after exiting to finalize versions.')); + } + + preManager.exit(); + + console.log(); + console.log(chalk.green('βœ“') + ' Exited pre-release mode'); + console.log(); + console.log(chalk.bold('Removed:')); + console.log(` ${chalk.red('-')} .contractual/pre.json`); + + if (state) { + console.log(); + console.log(chalk.dim(`Pre-release tag was: ${state.tag}`)); + console.log(chalk.dim(`Entered at: ${new Date(state.enteredAt).toLocaleString()}`)); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Failed to exit pre-release mode:'), message); + process.exitCode = 1; + } +} + +/** + * Show pre-release status + */ +export async function preStatusCommand(): Promise { + const cwd = process.cwd(); + const contractualDir = findContractualDir(cwd); + + if (!contractualDir) { + console.error(chalk.red('No .contractual directory found. Run `contractual init` first.')); + process.exitCode = 1; + return; + } + + const preManager = new PreReleaseManager(contractualDir); + + if (!preManager.isActive()) { + console.log(chalk.dim('Not in pre-release mode.')); + console.log(chalk.dim('Run `contractual pre enter ` to enter pre-release mode.')); + return; + } + + const state = preManager.getState(); + if (!state) { + console.log(chalk.yellow('Pre-release state file is corrupted.')); + console.log(chalk.dim('Run `contractual pre exit` to reset.')); + return; + } + + const versionManager = new VersionManager(contractualDir); + const currentVersions = versionManager.getAllVersions(); + + console.log(chalk.bold('Pre-release Status')); + console.log(); + console.log(` ${chalk.dim('Tag:')} ${chalk.cyan(state.tag)}`); + console.log(` ${chalk.dim('Since:')} ${new Date(state.enteredAt).toLocaleString()}`); + + // Show version changes since entering pre-release + const changedContracts = Object.entries(currentVersions).filter(([name, version]) => { + const initial = state.initialVersions[name]; + return initial && initial !== version; + }); + + if (changedContracts.length > 0) { + console.log(); + console.log(chalk.bold('Version changes since entering pre-release:')); + for (const [name, version] of changedContracts) { + const initial = state.initialVersions[name]; + console.log(` ${chalk.cyan(name)}: ${chalk.gray(initial)} β†’ ${chalk.green(version)}`); + } + } + + console.log(); + console.log(chalk.dim('Commands:')); + console.log(chalk.dim(' contractual version Apply changesets with pre-release versions')); + console.log(chalk.dim(' contractual pre exit Exit pre-release mode')); +} diff --git a/packages/cli/src/commands/status.command.ts b/packages/cli/src/commands/status.command.ts new file mode 100644 index 0000000..1782947 --- /dev/null +++ b/packages/cli/src/commands/status.command.ts @@ -0,0 +1,144 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import { loadConfig } from '../config/index.js'; +import { findContractualDir, VERSIONS_FILE, CHANGESETS_DIR } from '../utils/files.js'; +import { aggregateBumps, incrementVersion, readChangesets } from '@contractual/changesets'; +import type { VersionsFile, ChangesetFile } from '@contractual/types'; + +/** + * Read versions.json file + */ +function readVersions(contractualDir: string): VersionsFile { + const versionsPath = join(contractualDir, VERSIONS_FILE); + + if (!existsSync(versionsPath)) { + return {}; + } + + try { + const content = readFileSync(versionsPath, 'utf-8'); + return JSON.parse(content) as VersionsFile; + } catch { + return {}; + } +} + +/** + * Show current state - versions, pending changesets, projected bumps + */ +export async function statusCommand(): Promise { + try { + // Load config + const config = loadConfig(); + const contractualDir = findContractualDir(); + + if (!contractualDir) { + console.log(chalk.red('Not initialized:') + ' .contractual directory not found'); + console.log(chalk.dim('Run `contractual init` to get started')); + process.exitCode = 1; + return; + } + + // Read versions + const versions = readVersions(contractualDir); + + // Read pending changesets + const changesetsDir = join(contractualDir, CHANGESETS_DIR); + let changesets: ChangesetFile[] = []; + try { + changesets = await readChangesets(changesetsDir); + } catch (error) { + // If parsing fails, show a warning but continue + const message = error instanceof Error ? error.message : 'Unknown error'; + console.warn(chalk.yellow(`Warning: ${message}`)); + } + + // Calculate projected bumps + const projectedBumps = aggregateBumps(changesets); + + // Print header + console.log(chalk.bold('\nContractual Status\n')); + + // Print contract versions + console.log(chalk.bold.underline('Contracts')); + console.log(); + + if (config.contracts.length === 0) { + console.log(chalk.dim(' No contracts configured')); + } else { + for (const contract of config.contracts) { + const versionEntry = versions[contract.name]; + const currentVersion = versionEntry?.version ?? '0.0.0'; + const bump = projectedBumps[contract.name]; + + let projectedVersion: string | null = null; + if (bump) { + projectedVersion = incrementVersion(currentVersion, bump); + } + + // Contract name and type + const typeLabel = chalk.dim(`(${contract.type})`); + console.log(` ${chalk.cyan(contract.name)} ${typeLabel}`); + + // Version info + const versionLabel = versionEntry + ? chalk.green(`v${currentVersion}`) + : chalk.dim('v0.0.0 (unreleased)'); + + if (projectedVersion) { + const bumpColor = + bump === 'major' ? chalk.red : bump === 'minor' ? chalk.yellow : chalk.green; + console.log( + ` ${versionLabel} ${chalk.dim('->')} ${bumpColor(`v${projectedVersion}`)} ${chalk.dim(`(${bump})`)}` + ); + } else { + console.log(` ${versionLabel}`); + } + + // Release date + if (versionEntry?.released) { + const date = new Date(versionEntry.released).toLocaleDateString(); + console.log(` ${chalk.dim(`Released: ${date}`)}`); + } + + console.log(); + } + } + + // Print pending changesets + console.log(chalk.bold.underline('Pending Changesets')); + console.log(); + + if (changesets.length === 0) { + console.log(chalk.dim(' No pending changesets')); + console.log(chalk.dim(' Run `contractual add` to create one')); + } else { + console.log(` ${chalk.yellow(changesets.length.toString())} changeset(s) pending\n`); + + for (const changeset of changesets) { + console.log(` ${chalk.dim('-')} ${changeset.filename}`); + + for (const [contract, bump] of Object.entries(changeset.bumps)) { + const bumpColor = + bump === 'major' ? chalk.red : bump === 'minor' ? chalk.yellow : chalk.green; + console.log(` ${chalk.cyan(contract)}: ${bumpColor(bump)}`); + } + } + } + + console.log(); + + // Summary + const hasProjectedBumps = Object.keys(projectedBumps).length > 0; + if (hasProjectedBumps) { + console.log( + chalk.dim('Run `contractual version` to apply pending changesets and bump versions') + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red('Error:'), message); + process.exitCode = 1; + } +} diff --git a/packages/cli/src/commands/version.command.ts b/packages/cli/src/commands/version.command.ts new file mode 100644 index 0000000..43ffc08 --- /dev/null +++ b/packages/cli/src/commands/version.command.ts @@ -0,0 +1,351 @@ +import { existsSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import ora from 'ora'; +import { loadConfig } from '../config/index.js'; +import { findContractualDir, CHANGESETS_DIR } from '../utils/files.js'; +import { promptConfirm, type PromptOptions } from '../utils/prompts.js'; +import { + VersionManager, + PreReleaseManager, + readChangesets, + aggregateBumps, + extractContractChanges, + appendChangelog, + incrementVersion, + incrementVersionWithPreRelease, + updateSpecVersion, +} from '@contractual/changesets'; +import type { BumpResult, BumpType } from '@contractual/types'; + +/** + * Options for the version command + */ +interface VersionOptions extends PromptOptions { + /** Preview without applying */ + dryRun?: boolean; + /** Output JSON (implies --yes) */ + json?: boolean; + /** Skip updating version field inside spec files */ + syncVersion?: boolean; +} + +/** + * Pending version bump info + */ +interface PendingBump { + contract: string; + currentVersion: string; + nextVersion: string; + bumpType: BumpType; +} + +/** + * Consume changesets and bump versions + */ +export async function versionCommand(options: VersionOptions = {}): Promise { + // JSON output implies --yes (no prompts) + if (options.json) { + options.yes = true; + } + + const spinner = ora('Loading configuration...').start(); + + let config; + try { + config = loadConfig(); + spinner.succeed('Configuration loaded'); + } catch (error) { + spinner.fail('Failed to load configuration'); + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(chalk.red(message)); + process.exit(1); + } + + const contractualDir = findContractualDir(config.configDir); + if (!contractualDir) { + console.error(chalk.red('No .contractual directory found. Run `contractual init` first.')); + process.exit(1); + } + + // Read all changesets + if (config.versioning?.mode === 'fixed') { + throw new Error( + 'Fixed versioning is not implemented. Use versioning.mode: independent. No versions were changed.' + ); + } + + const readSpinner = ora('Reading changesets...').start(); + const changesetsDir = join(contractualDir, CHANGESETS_DIR); + const changesets = await readChangesets(changesetsDir); + + if (changesets.length === 0) { + readSpinner.succeed('No pending changesets'); + if (options.json) { + console.log(JSON.stringify({ bumps: [], changesets: 0 }, null, 2)); + } else { + console.log(chalk.gray('Nothing to version.')); + } + process.exit(0); + } + + readSpinner.succeed(`Found ${changesets.length} changeset(s)`); + + // Aggregate bumps (highest wins per contract) + const aggregatedBumps = aggregateBumps(changesets); + + const unknownContracts = Object.keys(aggregatedBumps).filter( + (name) => !config.contracts.some((contract) => contract.name === name) + ); + if (unknownContracts.length > 0) { + throw new Error( + `Changesets reference unknown contracts: ${unknownContracts.join(', ')}. No changesets were consumed.` + ); + } + + if (Object.keys(aggregatedBumps).length === 0) { + if (options.json) { + console.log(JSON.stringify({ bumps: [], changesets: changesets.length }, null, 2)); + } else { + console.log(chalk.gray('No version bumps required.')); + } + process.exit(0); + } + + // Initialize version manager for reading current versions + const versionManager = new VersionManager(contractualDir); + const preManager = new PreReleaseManager(contractualDir); + const preReleaseTag = preManager.getTag(); + + // Calculate pending bumps (preview) + const pendingBumps: PendingBump[] = []; + + for (const [contractName, bumpType] of Object.entries(aggregatedBumps)) { + const contract = config.contracts.find((c) => c.name === contractName); + if (!contract) { + continue; + } + + const currentVersion = versionManager.getVersion(contractName) ?? '0.0.0'; + const nextVersion = preReleaseTag + ? incrementVersionWithPreRelease(currentVersion, bumpType, preReleaseTag) + : incrementVersion(currentVersion, bumpType); + + pendingBumps.push({ + contract: contractName, + currentVersion, + nextVersion, + bumpType, + }); + } + + // Show pre-release mode notice + if (preReleaseTag && !options.json) { + console.log(chalk.cyan(`Pre-release mode: ${preReleaseTag}`)); + } + + // Show preview + if (options.json) { + if (options.dryRun) { + console.log( + JSON.stringify( + { + dryRun: true, + bumps: pendingBumps.map((b) => ({ + contract: b.contract, + current: b.currentVersion, + next: b.nextVersion, + type: b.bumpType, + })), + changesets: changesets.length, + }, + null, + 2 + ) + ); + return; + } + } else { + printPreviewTable(pendingBumps); + + if (options.dryRun) { + console.log(); + console.log(chalk.dim('Dry run - no changes applied')); + return; + } + } + + // Confirm before applying (unless --yes) + if (!options.json) { + const shouldApply = await promptConfirm('Apply these version bumps?', true, options); + + if (!shouldApply) { + console.log(chalk.dim('Cancelled')); + return; + } + } + + // Apply version bumps + const bumpSpinner = options.json ? null : ora('Applying version bumps...').start(); + const bumpResults: BumpResult[] = []; + const consumedChangesetPaths: string[] = []; + + for (const [contractName, bumpType] of Object.entries(aggregatedBumps)) { + const contract = config.contracts.find((c) => c.name === contractName); + if (!contract) { + if (!options.json) { + console.warn( + chalk.yellow(`Warning: Contract "${contractName}" not found in config, skipping.`) + ); + } + continue; + } + + const oldVersion = versionManager.getVersion(contractName) ?? '0.0.0'; + let newVersion: string; + + const shouldSyncVersion = options.syncVersion !== false && contract.syncVersion !== false; + + if (preReleaseTag) { + // Use pre-release version increment + newVersion = incrementVersionWithPreRelease(oldVersion, bumpType, preReleaseTag); + if (shouldSyncVersion) { + updateSpecVersion(contract.absolutePath, newVersion, contract.type); + } + versionManager.setVersion(contractName, newVersion, contract.absolutePath); + } else { + // Normal bump β€” compute version first, update spec, then bump (which copies to snapshots) + newVersion = incrementVersion(oldVersion, bumpType); + if (shouldSyncVersion) { + updateSpecVersion(contract.absolutePath, newVersion, contract.type); + } + versionManager.bump(contractName, bumpType, contract.absolutePath); + } + + // Extract changes text from changesets for this contract + const changes = extractContractChanges(changesets, contractName); + + bumpResults.push({ + contract: contractName, + oldVersion, + newVersion, + bumpType, + changes, + }); + } + + bumpSpinner?.succeed('Version bumps applied'); + + // Append to CHANGELOG.md + const changelogSpinner = options.json ? null : ora('Updating changelog...').start(); + const changelogPath = join(config.configDir, 'CHANGELOG.md'); + try { + appendChangelog(changelogPath, bumpResults); + changelogSpinner?.succeed('Changelog updated'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + changelogSpinner?.warn(`Failed to update changelog: ${message}`); + } + + // Delete consumed changeset files + const cleanupSpinner = options.json ? null : ora('Cleaning up changesets...').start(); + + for (const changeset of changesets) { + const changesetPath = join(changesetsDir, changeset.filename); + try { + if (existsSync(changesetPath)) { + unlinkSync(changesetPath); + consumedChangesetPaths.push(changeset.filename); + } + } catch { + // Ignore cleanup errors + } + } + cleanupSpinner?.succeed(`Removed ${consumedChangesetPaths.length} changeset(s)`); + + // Print summary + if (options.json) { + console.log( + JSON.stringify( + { + bumps: bumpResults.map((r) => ({ + contract: r.contract, + old: r.oldVersion, + new: r.newVersion, + type: r.bumpType, + })), + changesets: consumedChangesetPaths.length, + }, + null, + 2 + ) + ); + } else { + console.log(); + console.log(chalk.bold('Version Summary:')); + console.log(); + + for (const result of bumpResults) { + console.log( + ` ${chalk.cyan(result.contract)}: ` + + `${chalk.gray(result.oldVersion)} -> ${chalk.green(result.newVersion)} ` + + `(${result.bumpType})` + ); + } + + console.log(); + console.log(chalk.green('Done!'), `${bumpResults.length} contract(s) versioned.`); + } +} + +/** + * Print a preview table of pending version bumps + */ +function printPreviewTable(bumps: PendingBump[]): void { + console.log(); + console.log(chalk.bold('Pending version bumps:')); + console.log(); + + // Calculate column widths + const maxContractLen = Math.max(8, ...bumps.map((b) => b.contract.length)); + const maxCurrentLen = Math.max(7, ...bumps.map((b) => b.currentVersion.length)); + const maxNextLen = Math.max(4, ...bumps.map((b) => b.nextVersion.length)); + + // Header + const header = + ` ${'Contract'.padEnd(maxContractLen)} ` + + `${'Current'.padEnd(maxCurrentLen)} ` + + `${'β†’'} ` + + `${'Next'.padEnd(maxNextLen)} ` + + `Reason`; + console.log(chalk.dim(header)); + console.log(chalk.dim(' ' + '─'.repeat(header.length - 2))); + + // Rows + for (const bump of bumps) { + const reason = getBumpReason(bump.bumpType); + console.log( + ` ${chalk.cyan(bump.contract.padEnd(maxContractLen))} ` + + `${chalk.gray(bump.currentVersion.padEnd(maxCurrentLen))} ` + + `${chalk.dim('β†’')} ` + + `${chalk.green(bump.nextVersion.padEnd(maxNextLen))} ` + + `${reason}` + ); + } +} + +/** + * Get human-readable reason for bump type + */ +function getBumpReason(bumpType: BumpType): string { + switch (bumpType) { + case 'major': + return chalk.red('major (breaking)'); + case 'minor': + return chalk.yellow('minor (feature)'); + case 'patch': + return chalk.dim('patch (fix)'); + default: + return bumpType; + } +} diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts new file mode 100644 index 0000000..1febe5a --- /dev/null +++ b/packages/cli/src/config/index.ts @@ -0,0 +1,2 @@ +export * from './loader.js'; +export * from './validator.js'; diff --git a/packages/cli/src/config/loader.ts b/packages/cli/src/config/loader.ts new file mode 100644 index 0000000..ba8633b --- /dev/null +++ b/packages/cli/src/config/loader.ts @@ -0,0 +1,204 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import fg from 'fast-glob'; +import type { ContractualConfig, ResolvedConfig, ResolvedContract } from '@contractual/types'; +import { validateConfig, formatValidationErrors } from './validator.js'; + +/** + * Config file names to search for + */ +const CONFIG_FILENAMES = ['contractual.yaml', 'contractual.yml']; + +// Bound the input and recursive AST depth before fast-glob invokes braces. +const MAX_GLOB_PATTERN_LENGTH = 1024; +const MAX_GLOB_NESTING = 32; + +/** + * Error thrown when config cannot be loaded + */ +export class ConfigError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConfigError'; + } +} + +function validateGlobPattern(pattern: string, contractName: string): void { + if (pattern.length > MAX_GLOB_PATTERN_LENGTH) { + throw new ConfigError( + `Glob for contract "${contractName}" exceeds ${MAX_GLOB_PATTERN_LENGTH} characters after resolving its path` + ); + } + + const nesting: string[] = []; + let quote: string | undefined; + let brackets = 0; + + for (let index = 0; index < pattern.length; index++) { + const character = pattern[index]; + if (character === '\\') { + index++; + continue; + } + if (quote) { + if (character === quote) quote = undefined; + continue; + } + if (brackets > 0) { + if (character === '[') brackets++; + if (character === ']') brackets--; + continue; + } + if (character === '[') { + brackets++; + continue; + } + if (character === '"' || character === "'" || character === '`') { + quote = character; + continue; + } + // Parentheses also create recursive nodes in the braces parser. Only a + // matching closer ends a node; quoted/class/escaped closers are literals. + if (character === '{' || character === '(') { + nesting.push(character); + if (nesting.length > MAX_GLOB_NESTING) { + throw new ConfigError( + `Glob for contract "${contractName}" exceeds ${MAX_GLOB_NESTING} levels of brace/parenthesis nesting` + ); + } + } else if ( + (character === '}' && nesting.at(-1) === '{') || + (character === ')' && nesting.at(-1) === '(') + ) { + nesting.pop(); + } + } +} + +/** + * Find contractual.yaml by walking up from the given directory + */ +export function findConfigFile(startDir: string = process.cwd()): string | null { + let currentDir = resolve(startDir); + const root = dirname(currentDir); + + while (currentDir !== root) { + for (const filename of CONFIG_FILENAMES) { + const configPath = join(currentDir, filename); + if (existsSync(configPath)) { + return configPath; + } + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; + } + + // Check root directory too + for (const filename of CONFIG_FILENAMES) { + const configPath = join(currentDir, filename); + if (existsSync(configPath)) { + return configPath; + } + } + + return null; +} + +/** + * Parse YAML config file + */ +export function parseConfigFile(configPath: string): unknown { + const content = readFileSync(configPath, 'utf-8'); + + try { + return parseYaml(content); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown parse error'; + throw new ConfigError(`Failed to parse ${configPath}: ${message}`); + } +} + +/** + * Resolve contract paths to absolute paths + */ +export function resolveContractPaths( + config: ContractualConfig, + configDir: string +): ResolvedContract[] { + const resolved: ResolvedContract[] = []; + + for (const contract of config.contracts) { + const pattern = contract.path; + const absolutePattern = resolve(configDir, pattern); + + // Check if it's a glob pattern + if (pattern.includes('*')) { + validateGlobPattern(absolutePattern, contract.name); + const matches = fg.sync(absolutePattern, { onlyFiles: true }); + if (matches.length === 0) { + console.warn( + `Warning: No files matched pattern "${pattern}" for contract "${contract.name}"` + ); + } + // For glob patterns, use the first match as the primary path + // In the future, we might support multiple specs per contract + if (matches.length > 0) { + resolved.push({ + ...contract, + absolutePath: matches[0], + }); + } + } else { + // Direct file path + if (!existsSync(absolutePattern)) { + console.warn(`Warning: File not found "${pattern}" for contract "${contract.name}"`); + } + resolved.push({ + ...contract, + absolutePath: absolutePattern, + }); + } + } + + return resolved; +} + +/** + * Load and validate config from a file path + */ +export function loadConfigFromPath(configPath: string): ResolvedConfig { + const parsed = parseConfigFile(configPath); + + const validation = validateConfig(parsed); + if (!validation.valid) { + throw new ConfigError( + `Invalid configuration in ${configPath}:\n${formatValidationErrors(validation.errors)}` + ); + } + + const config = parsed as ContractualConfig; + const configDir = dirname(configPath); + const resolvedContracts = resolveContractPaths(config, configDir); + + return { + ...config, + contracts: resolvedContracts, + configDir, + configPath, + }; +} + +/** + * Load config by searching from the current directory + */ +export function loadConfig(startDir?: string): ResolvedConfig { + const configPath = findConfigFile(startDir); + + if (!configPath) { + throw new ConfigError('No contractual.yaml found. Run `contractual init` to get started.'); + } + + return loadConfigFromPath(configPath); +} diff --git a/packages/cli/src/config/schema.json b/packages/cli/src/config/schema.json new file mode 100644 index 0000000..2b59b0d --- /dev/null +++ b/packages/cli/src/config/schema.json @@ -0,0 +1,126 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://contractual.dev/schemas/config.json", + "title": "Contractual Configuration", + "description": "Configuration file for Contractual schema contract lifecycle orchestrator", + "type": "object", + "required": ["contracts"], + "properties": { + "contracts": { + "type": "array", + "description": "List of contract definitions", + "minItems": 1, + "items": { + "type": "object", + "required": ["name", "type", "path"], + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for the contract", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "type": { + "type": "string", + "description": "Type of schema", + "enum": ["openapi", "json-schema", "asyncapi", "odcs"] + }, + "path": { + "type": "string", + "description": "Path to spec file (can be glob pattern)" + }, + "lint": { + "oneOf": [ + { "type": "string", "description": "Linter tool name or custom command" }, + { "type": "boolean", "const": false, "description": "Disable linting" } + ] + }, + "breaking": { + "oneOf": [ + { "type": "string", "description": "Differ tool name or custom command" }, + { "type": "boolean", "const": false, "description": "Disable breaking change detection" } + ] + }, + "syncVersion": { + "type": "boolean", + "description": "Sync version field inside the spec file on version bump (default: true). Set to false to skip.", + "default": true + }, + "generate": { + "type": "array", + "description": "Output generation commands", + "items": { "type": "string" } + } + }, + "additionalProperties": false + } + }, + "versioning": { + "type": "object", + "description": "Versioning configuration", + "properties": { + "mode": { + "type": "string", + "enum": ["independent", "fixed"], + "description": "Versioning mode: independent (each contract separate) or fixed (all share same version)", + "default": "independent" + } + }, + "additionalProperties": false + }, + "changeset": { + "type": "object", + "description": "Changeset behavior configuration", + "properties": { + "autoDetect": { + "type": "boolean", + "description": "Auto-detect change classifications", + "default": true + }, + "requireOnPR": { + "type": "boolean", + "description": "Require changeset for spec changes in PRs", + "default": true + } + }, + "additionalProperties": false + }, + "ai": { + "type": "object", + "description": "AI/LLM integration configuration", + "properties": { + "provider": { + "type": "string", + "enum": ["anthropic"], + "default": "anthropic" + }, + "model": { + "type": "string", + "description": "Model to use" + }, + "features": { + "type": "object", + "properties": { + "explain": { + "type": "boolean", + "description": "PR change explanations", + "default": true + }, + "changelog": { + "type": "boolean", + "description": "AI-enriched changelog entries", + "default": true + }, + "enhance": { + "type": "boolean", + "description": "Spec metadata enhancement", + "default": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/packages/cli/src/config/validator.ts b/packages/cli/src/config/validator.ts new file mode 100644 index 0000000..c152a43 --- /dev/null +++ b/packages/cli/src/config/validator.ts @@ -0,0 +1,58 @@ +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +const AjvConstructor = Ajv.default ?? Ajv; +const addFormatsFunc = addFormats.default ?? addFormats; +import type { ContractualConfig } from '@contractual/types'; +import configSchema from './schema.json' with { type: 'json' }; + +/** + * Validation error with path and message + */ +export interface ValidationError { + path: string; + message: string; +} + +/** + * Result of config validation + */ +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; +} + +/** + * Validates a parsed config object against the JSON schema + */ +export function validateConfig(config: unknown): ValidationResult { + const ajv = new AjvConstructor({ allErrors: true, strict: false }); + addFormatsFunc(ajv); + + const validate = ajv.compile(configSchema); + const valid = validate(config); + + if (valid) { + return { valid: true, errors: [] }; + } + + const errors: ValidationError[] = (validate.errors ?? []).map((err) => ({ + path: err.instancePath || '/', + message: err.message ?? 'Unknown validation error', + })); + + return { valid: false, errors }; +} + +/** + * Type guard to check if config is valid ContractualConfig + */ +export function isValidConfig(config: unknown): config is ContractualConfig { + return validateConfig(config).valid; +} + +/** + * Format validation errors for display + */ +export function formatValidationErrors(errors: ValidationError[]): string { + return errors.map((err) => ` ${err.path}: ${err.message}`).join('\n'); +} diff --git a/packages/cli/src/core/diff.ts b/packages/cli/src/core/diff.ts new file mode 100644 index 0000000..44cddec --- /dev/null +++ b/packages/cli/src/core/diff.ts @@ -0,0 +1,129 @@ +/** + * Core diffing logic + * + * Shared module for running diffs across contracts. + * Used by `diff`, `breaking`, and `changeset` commands. + */ + +import type { ResolvedConfig, ResolvedContract, DiffResult } from '@contractual/types'; +import { getDiffer } from '../governance/index.js'; +import { findContractualDir, getSnapshotPath } from '../utils/files.js'; + +export interface DiffOptions { + /** Filter to specific contract names */ + contracts?: string[]; + /** Include contracts with no changes in results */ + includeEmpty?: boolean; +} + +export interface DiffContractsResult { + results: DiffResult[]; + contractualDir: string; +} + +/** + * Core diffing function. Runs the appropriate differ for each contract, + * comparing current spec against last versioned snapshot. + * + * Returns classified changes for all contracts. + * This is the primitive that `diff`, `breaking`, and `changeset` all use. + */ +export async function diffContracts( + config: ResolvedConfig, + options: DiffOptions = {} +): Promise { + const contractualDir = findContractualDir(config.configDir); + if (!contractualDir) { + throw new Error('No .contractual directory found. Run `contractual init` first.'); + } + + // Filter contracts if specific ones requested + let contracts: ResolvedContract[] = config.contracts; + if (options.contracts && options.contracts.length > 0) { + contracts = config.contracts.filter((c) => options.contracts!.includes(c.name)); + + // Check for missing contracts + for (const name of options.contracts) { + if (!config.contracts.some((c) => c.name === name)) { + throw new Error(`Contract "${name}" not found in configuration.`); + } + } + } + + const results: DiffResult[] = []; + + for (const contract of contracts) { + const result = await diffSingleContract(contract, contractualDir); + + // Include result if it has changes, or if includeEmpty is true + if (result.changes.length > 0 || options.includeEmpty) { + results.push(result); + } + } + + return { results, contractualDir }; +} + +/** + * Diff a single contract against its snapshot + */ +async function diffSingleContract( + contract: ResolvedContract, + contractualDir: string +): Promise { + // Get snapshot path + const snapshotPath = getSnapshotPath(contract.name, contractualDir); + + // No snapshot = first version, no changes + if (!snapshotPath) { + return createEmptyResult(contract.name, 'first-version'); + } + + // Check if breaking detection is disabled + if (contract.breaking === false) { + return createEmptyResult(contract.name, 'disabled'); + } + + // Get the differ from the registry + const differ = getDiffer(contract.type, contract.breaking); + + if (differ === null) { + throw new Error( + `No differ available for ${contract.type} contract "${contract.name}". Configure a custom breaking command or set breaking: false explicitly.` + ); + } + + if (!differ) { + // No differ registered for this type + return createEmptyResult(contract.name, 'no-differ'); + } + + // Run the differ: snapshot (old) vs current spec (new) + const diffResult = await differ(snapshotPath, contract.absolutePath); + + // Override contract name to match config + return { + ...diffResult, + contract: contract.name, + }; +} + +/** + * Create an empty diff result + */ +function createEmptyResult( + contractName: string, + _reason: 'first-version' | 'disabled' | 'no-differ' +): DiffResult { + return { + contract: contractName, + changes: [], + summary: { + breaking: 0, + nonBreaking: 0, + patch: 0, + unknown: 0, + }, + suggestedBump: 'none', + }; +} diff --git a/packages/cli/src/formatters/diff.ts b/packages/cli/src/formatters/diff.ts new file mode 100644 index 0000000..ff1b495 --- /dev/null +++ b/packages/cli/src/formatters/diff.ts @@ -0,0 +1,164 @@ +/** + * Diff output formatters + * + * Format diff results for text and JSON output. + */ + +import chalk from 'chalk'; +import type { DiffResult, Change, ChangeSeverity, DiffSummary } from '@contractual/types'; + +export interface FormatOptions { + /** Show JSON Pointer paths for each change */ + verbose?: boolean; +} + +/** + * Format diff results as human-readable text + */ +export function formatDiffText(results: DiffResult[], options: FormatOptions = {}): void { + if (results.length === 0) { + console.log(chalk.dim('No contracts to diff.')); + return; + } + + for (const result of results) { + formatContractResult(result, options); + } +} + +/** + * Format a single contract's diff result + */ +function formatContractResult(result: DiffResult, options: FormatOptions): void { + if (result.changes.length === 0) { + console.log(`${chalk.cyan(result.contract)}: ${chalk.dim('no changes')}`); + console.log(); + return; + } + + // Build summary parts + const parts: string[] = []; + if (result.summary.breaking > 0) { + parts.push(`${result.summary.breaking} breaking`); + } + if (result.summary.nonBreaking > 0) { + parts.push(`${result.summary.nonBreaking} non-breaking`); + } + if (result.summary.patch > 0) { + parts.push(`${result.summary.patch} patch`); + } + if (result.summary.unknown > 0) { + parts.push(`${result.summary.unknown} unknown`); + } + + // Color for suggested bump + const bumpColor = + result.suggestedBump === 'major' + ? chalk.red + : result.suggestedBump === 'minor' + ? chalk.yellow + : chalk.green; + + // Header line + console.log( + `${chalk.cyan(result.contract)}: ${result.changes.length} change(s) (${parts.join(', ')}) β€” suggested bump: ${bumpColor(result.suggestedBump)}` + ); + console.log(); + + // Each change + for (const change of result.changes) { + const label = formatSeverityLabel(change.severity); + console.log(` ${label} ${change.message}`); + if (options.verbose && change.path) { + console.log(`${' '.repeat(14)}path: ${chalk.dim(change.path)}`); + } + } + console.log(); +} + +/** + * Format severity as a colored, padded label + */ +function formatSeverityLabel(severity: ChangeSeverity): string { + switch (severity) { + case 'breaking': + return chalk.red.bold('BREAKING'.padEnd(12)); + case 'non-breaking': + return chalk.yellow('non-breaking'); + case 'patch': + return chalk.green('patch'.padEnd(12)); + case 'unknown': + return chalk.gray('unknown'.padEnd(12)); + default: + return chalk.gray(String(severity).padEnd(12)); + } +} + +/** + * Format diff results as JSON + * + * Output format: + * { + * "contracts": { + * "order": { "changes": [...], "summary": {...}, "suggestedBump": "minor" }, + * "petstore": { "changes": [...], "summary": {...}, "suggestedBump": "none" } + * } + * } + */ +export function formatDiffJson(results: DiffResult[]): string { + const contracts: Record> = {}; + + for (const result of results) { + const { contract, ...rest } = result; + contracts[contract] = rest; + } + + return JSON.stringify({ contracts }, null, 2); +} + +/** + * Filter results by severity level + */ +export function filterBySeverity(results: DiffResult[], severity: string): DiffResult[] { + if (severity === 'all') { + return results; + } + + return results.map((r) => { + const filteredChanges = r.changes.filter((c) => c.severity === severity); + return { + ...r, + changes: filteredChanges, + summary: recalculateSummary(filteredChanges), + suggestedBump: calculateSuggestedBump(filteredChanges), + }; + }); +} + +/** + * Recalculate summary from filtered changes + */ +function recalculateSummary(changes: Change[]): DiffSummary { + return { + breaking: changes.filter((c) => c.severity === 'breaking').length, + nonBreaking: changes.filter((c) => c.severity === 'non-breaking').length, + patch: changes.filter((c) => c.severity === 'patch').length, + unknown: changes.filter((c) => c.severity === 'unknown').length, + }; +} + +/** + * Calculate suggested bump from changes + */ +function calculateSuggestedBump(changes: Change[]): 'major' | 'minor' | 'patch' | 'none' { + if (changes.some((c) => c.severity === 'breaking')) { + return 'major'; + } + if (changes.some((c) => c.severity === 'non-breaking')) { + return 'minor'; + } + if (changes.some((c) => c.severity === 'patch')) { + return 'patch'; + } + return 'none'; +} diff --git a/packages/cli/src/governance/index.ts b/packages/cli/src/governance/index.ts new file mode 100644 index 0000000..7148303 --- /dev/null +++ b/packages/cli/src/governance/index.ts @@ -0,0 +1,31 @@ +/** + * Re-export governance engines from @contractual/governance + * + * This module re-exports the governance registry functions and auto-registers + * all built-in engines on import. + */ + +// Import to trigger auto-registration of all engines +import '@contractual/governance'; + +// Re-export registry functions +export { + registerLinter, + registerDiffer, + getLinter, + getDiffer, + hasLinter, + hasDiffer, + getRegisteredLinterTypes, + getRegisteredDifferTypes, +} from '@contractual/governance'; + +// Re-export individual engines for direct use +export { lintOpenAPI, lintJsonSchema, diffOpenApi, diffJsonSchema } from '@contractual/governance'; + +// Re-export runner utilities for custom commands +export { + executeCustomCommand, + parseCustomLintOutput, + parseCustomDiffOutput, +} from '@contractual/governance'; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d0d3bbd..cc6993d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1 +1,38 @@ -export * from './commands.js'; +// Public API for programmatic use +export { loadConfig, ConfigError } from './config/index.js'; +export { registerLinter, registerDiffer, getLinter, getDiffer } from './governance/index.js'; + +// Core diffing +export { diffContracts } from './core/diff.js'; +export type { DiffOptions, DiffContractsResult } from './core/diff.js'; + +// File utilities +export { + findContractualDir, + getSnapshotPath, + copyToSnapshots, + CONTRACTUAL_DIR, +} from './utils/files.js'; + +// Re-export from @contractual/changesets +export { + VersionManager, + PreReleaseManager, + createChangeset, + readChangesets, + aggregateBumps, + generateChangesetName, + extractContractChanges, + appendChangelog, + incrementVersion, + incrementVersionWithPreRelease, + VERSIONS_FILE, + SNAPSHOTS_DIR, + CHANGESETS_DIR, + PRE_RELEASE_FILE, + updateSpecVersion, +} from '@contractual/changesets'; +export type { BumpOperationResult } from '@contractual/changesets'; + +// Re-export types from @contractual/types +export type * from '@contractual/types'; diff --git a/packages/cli/src/utils/exec.ts b/packages/cli/src/utils/exec.ts new file mode 100644 index 0000000..8c0a540 --- /dev/null +++ b/packages/cli/src/utils/exec.ts @@ -0,0 +1,105 @@ +import { execSync, type ExecSyncOptions } from 'node:child_process'; + +/** + * Result of executing a command + */ +export interface ExecResult { + /** Standard output */ + stdout: string; + /** Standard error */ + stderr: string; + /** Exit code */ + exitCode: number; +} + +/** + * Error shape from execSync when command fails + */ +interface ExecSyncError { + stdout?: Buffer | string; + stderr?: Buffer | string; + status?: number; +} + +/** + * Type guard for execSync errors + */ +function isExecSyncError(err: unknown): err is ExecSyncError { + return ( + typeof err === 'object' && + err !== null && + ('status' in err || 'stdout' in err || 'stderr' in err) + ); +} + +/** + * Substitute placeholders in a command string + */ +export function substitutePlaceholders( + command: string, + substitutions: Record +): string { + let result = command; + for (const [key, value] of Object.entries(substitutions)) { + result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value); + } + return result; +} + +/** + * Check if a string contains placeholders + */ +export function hasPlaceholders(command: string): boolean { + return /\{(spec|old|new)\}/.test(command); +} + +/** + * Execute a command and return the result + */ +export function execCommand( + command: string, + substitutions: Record = {}, + options: ExecSyncOptions = {} +): ExecResult { + const resolvedCommand = substitutePlaceholders(command, substitutions); + + try { + const stdout = execSync(resolvedCommand, { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + ...options, + }) as string; + return { stdout, stderr: '', exitCode: 0 }; + } catch (err: unknown) { + if (isExecSyncError(err)) { + const stdout = typeof err.stdout === 'string' ? err.stdout : (err.stdout?.toString() ?? ''); + const stderr = typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? ''); + return { + stdout, + stderr, + exitCode: err.status ?? 1, + }; + } + // Unknown error type - return generic failure + return { + stdout: '', + stderr: err instanceof Error ? err.message : 'Unknown error', + exitCode: 1, + }; + } +} + +/** + * Execute a command and throw if it fails + */ +export function execCommandOrThrow( + command: string, + substitutions: Record = {}, + options: ExecSyncOptions = {} +): string { + const result = execCommand(command, substitutions, options); + if (result.exitCode !== 0) { + throw new Error(`Command failed with exit code ${result.exitCode}: ${result.stderr}`); + } + return result.stdout; +} diff --git a/packages/cli/src/utils/files.ts b/packages/cli/src/utils/files.ts new file mode 100644 index 0000000..d73fd4f --- /dev/null +++ b/packages/cli/src/utils/files.ts @@ -0,0 +1,160 @@ +import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync } from 'node:fs'; +import { dirname, extname, join, resolve } from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import type { ContractType } from '@contractual/types'; +import { CHANGESETS_DIR, SNAPSHOTS_DIR, VERSIONS_FILE } from '@contractual/changesets'; + +/** + * The .contractual directory name + */ +export const CONTRACTUAL_DIR = '.contractual'; + +// Re-export constants from @contractual/changesets for backward compatibility +export { CHANGESETS_DIR, SNAPSHOTS_DIR, VERSIONS_FILE }; + +/** + * Find the .contractual directory starting from a given path + */ +export function findContractualDir(startDir: string = process.cwd()): string | null { + let currentDir = resolve(startDir); + const root = dirname(currentDir); + + while (currentDir !== root) { + const contractualDir = join(currentDir, CONTRACTUAL_DIR); + if (existsSync(contractualDir)) { + return contractualDir; + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; + } + + // Check root directory too + const contractualDir = join(currentDir, CONTRACTUAL_DIR); + if (existsSync(contractualDir)) { + return contractualDir; + } + + return null; +} + +/** + * Ensure the .contractual directory structure exists + */ +export function ensureContractualDir(baseDir: string): string { + const contractualDir = join(baseDir, CONTRACTUAL_DIR); + const changesetsDir = join(contractualDir, CHANGESETS_DIR); + const snapshotsDir = join(contractualDir, SNAPSHOTS_DIR); + + mkdirSync(changesetsDir, { recursive: true }); + mkdirSync(snapshotsDir, { recursive: true }); + + // Create versions.json if it doesn't exist + const versionsPath = join(contractualDir, VERSIONS_FILE); + if (!existsSync(versionsPath)) { + writeFileSync(versionsPath, '{}', 'utf-8'); + } + + // Create .gitkeep files + const changesetsGitkeep = join(changesetsDir, '.gitkeep'); + const snapshotsGitkeep = join(snapshotsDir, '.gitkeep'); + if (!existsSync(changesetsGitkeep)) { + writeFileSync(changesetsGitkeep, '', 'utf-8'); + } + if (!existsSync(snapshotsGitkeep)) { + writeFileSync(snapshotsGitkeep, '', 'utf-8'); + } + + return contractualDir; +} + +/** + * Read and parse a spec file (YAML or JSON) + */ +export function readSpecFile(filePath: string): unknown { + const content = readFileSync(filePath, 'utf-8'); + const ext = extname(filePath).toLowerCase(); + + if (ext === '.json') { + return JSON.parse(content); + } + + // YAML (handles .yaml, .yml) + return parseYaml(content); +} + +/** + * Detect spec type from file path and content + */ +export function detectSpecType(filePath: string): ContractType | null { + // Check extension patterns first + if (/\.openapi\.(ya?ml|json)$/i.test(filePath)) return 'openapi'; + if (/\.asyncapi\.(ya?ml|json)$/i.test(filePath)) return 'asyncapi'; + if (/\.odcs\.ya?ml$/i.test(filePath)) return 'odcs'; + if (/\.schema\.json$/i.test(filePath)) return 'json-schema'; + + // Content sniffing + try { + const spec = readSpecFile(filePath) as Record; + if (!spec || typeof spec !== 'object') return null; + + // OpenAPI detection + if ('openapi' in spec || 'swagger' in spec) return 'openapi'; + + // AsyncAPI detection + if ('asyncapi' in spec) return 'asyncapi'; + + // ODCS detection + if ('dataContractSpecification' in spec || spec.kind === 'DataContract') return 'odcs'; + + // JSON Schema detection + if ( + ('$schema' in spec && String(spec.$schema).includes('json-schema')) || + ('type' in spec && 'properties' in spec) + ) + return 'json-schema'; + + return null; + } catch { + return null; + } +} + +/** + * Get the file extension for a contract type + */ +export function getSpecExtension(filePath: string): string { + const ext = extname(filePath).toLowerCase(); + return ext || '.yaml'; +} + +/** + * Copy a spec file to the snapshots directory + */ +export function copyToSnapshots( + specPath: string, + contractName: string, + contractualDir: string +): string { + const ext = getSpecExtension(specPath); + const snapshotPath = join(contractualDir, SNAPSHOTS_DIR, `${contractName}${ext}`); + copyFileSync(specPath, snapshotPath); + return snapshotPath; +} + +/** + * Get the snapshot path for a contract + */ +export function getSnapshotPath(contractName: string, contractualDir: string): string | null { + const snapshotsDir = join(contractualDir, SNAPSHOTS_DIR); + const extensions = ['.yaml', '.yml', '.json']; + + for (const ext of extensions) { + const snapshotPath = join(snapshotsDir, `${contractName}${ext}`); + if (existsSync(snapshotPath)) { + return snapshotPath; + } + } + + return null; +} diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts new file mode 100644 index 0000000..5741bd7 --- /dev/null +++ b/packages/cli/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './exec.js'; +export * from './files.js'; +export * from './output.js'; diff --git a/packages/cli/src/utils/output.ts b/packages/cli/src/utils/output.ts new file mode 100644 index 0000000..8f23bbe --- /dev/null +++ b/packages/cli/src/utils/output.ts @@ -0,0 +1,87 @@ +import chalk from 'chalk'; + +/** + * Print a success message with green checkmark + */ +export function printSuccess(message: string): void { + console.log(chalk.green('βœ“') + ' ' + message); +} + +/** + * Print an error message with red X + */ +export function printError(message: string): void { + console.log(chalk.red('βœ—') + ' ' + message); +} + +/** + * Print a warning message with yellow warning symbol + */ +export function printWarning(message: string): void { + console.log(chalk.yellow('⚠') + ' ' + message); +} + +/** + * Print an info message with blue info symbol + */ +export function printInfo(message: string): void { + console.log(chalk.blue('β„Ή') + ' ' + message); +} + +/** + * Print a simple ASCII table + */ +export function printTable(headers: string[], rows: string[][]): void { + if (headers.length === 0) { + return; + } + + // Calculate column widths + const columnWidths = headers.map((header, index) => { + const maxRowWidth = rows.reduce((max, row) => { + const cellLength = row[index]?.length ?? 0; + return Math.max(max, cellLength); + }, 0); + return Math.max(header.length, maxRowWidth); + }); + + // Create separator line + const separator = '+' + columnWidths.map((width) => '-'.repeat(width + 2)).join('+') + '+'; + + // Format a row + const formatRow = (cells: string[]): string => { + return ( + '|' + + cells.map((cell, index) => ' ' + (cell ?? '').padEnd(columnWidths[index]) + ' ').join('|') + + '|' + ); + }; + + // Print table + console.log(separator); + console.log(formatRow(headers)); + console.log(separator); + for (const row of rows) { + console.log(formatRow(row)); + } + console.log(separator); +} + +/** + * Format severity level with appropriate color + */ +export function formatSeverity( + severity: 'breaking' | 'non-breaking' | 'patch' | 'unknown' +): string { + switch (severity) { + case 'breaking': + return chalk.red.bold('BREAKING'); + case 'non-breaking': + return chalk.yellow('NON-BREAKING'); + case 'patch': + return chalk.green('PATCH'); + case 'unknown': + default: + return chalk.gray('UNKNOWN'); + } +} diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts new file mode 100644 index 0000000..5d135f7 --- /dev/null +++ b/packages/cli/src/utils/prompts.ts @@ -0,0 +1,160 @@ +import { select, input, confirm } from '@inquirer/prompts'; + +/** + * Check if running in interactive mode (TTY available) + */ +export function isInteractive(): boolean { + return process.stdin.isTTY === true && process.stdout.isTTY === true; +} + +/** + * Check if running in CI environment + */ +export function isCI(): boolean { + return ( + process.env.CI === 'true' || + process.env.CI === '1' || + process.env.CONTINUOUS_INTEGRATION === 'true' || + process.env.GITHUB_ACTIONS === 'true' || + process.env.GITLAB_CI === 'true' || + process.env.CIRCLECI === 'true' + ); +} + +/** + * Options for prompt functions + */ +export interface PromptOptions { + /** Skip prompts and use defaults (--yes flag) */ + yes?: boolean; + /** Force interactive mode even in CI */ + interactive?: boolean; +} + +/** + * Determine if prompts should be shown + */ +export function shouldPrompt(options: PromptOptions): boolean { + if (options.yes) return false; + if (options.interactive) return true; + if (isCI()) return false; + return isInteractive(); +} + +/** + * Prompt for a selection from a list of choices + */ +export async function promptSelect( + message: string, + choices: Array<{ value: T; name: string; description?: string }>, + defaultValue: T, + options: PromptOptions = {} +): Promise { + if (!shouldPrompt(options)) { + return defaultValue; + } + + return select({ + message, + choices: choices.map((c) => ({ + value: c.value, + name: c.name, + description: c.description, + })), + default: defaultValue, + }); +} + +/** + * Prompt for text input + */ +export async function promptInput( + message: string, + defaultValue: string, + options: PromptOptions = {} +): Promise { + if (!shouldPrompt(options)) { + return defaultValue; + } + + return input({ + message, + default: defaultValue, + }); +} + +/** + * Prompt for confirmation (yes/no) + */ +export async function promptConfirm( + message: string, + defaultValue: boolean, + options: PromptOptions = {} +): Promise { + if (!shouldPrompt(options)) { + return defaultValue; + } + + return confirm({ + message, + default: defaultValue, + }); +} + +/** + * Prompt for version input with validation + */ +export async function promptVersion( + message: string, + defaultValue: string, + options: PromptOptions = {} +): Promise { + if (!shouldPrompt(options)) { + return defaultValue; + } + + const result = await input({ + message, + default: defaultValue, + validate: (value) => { + const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/; + if (semverRegex.test(value)) { + return true; + } + return 'Please enter a valid semver version (e.g., 0.0.0, 1.2.3-beta.1)'; + }, + }); + + return result; +} + +/** + * Common version choices for init + */ +export const VERSION_CHOICES = [ + { value: '0.0.0', name: '0.0.0', description: 'Start fresh (recommended for new projects)' }, + { value: '1.0.0', name: '1.0.0', description: 'Production-ready' }, + { value: 'custom', name: 'Custom', description: 'Enter a custom version' }, +] as const; + +/** + * Common versioning mode choices + */ +export const VERSIONING_MODE_CHOICES = [ + { + value: 'independent', + name: 'Independent', + description: 'Each contract versioned separately', + }, + { value: 'fixed', name: 'Fixed', description: 'All contracts share same version' }, +] as const; + +/** + * Contract type choices + */ +export const CONTRACT_TYPE_CHOICES = [ + { value: 'openapi', name: 'OpenAPI', description: 'OpenAPI/Swagger specification' }, + { value: 'asyncapi', name: 'AsyncAPI', description: 'AsyncAPI specification' }, + { value: 'json-schema', name: 'JSON Schema', description: 'JSON Schema definition' }, + { value: 'odcs', name: 'ODCS', description: 'Open Data Contract Standard' }, +] as const; diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 5695712..ad568f0 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -3,16 +3,14 @@ "compilerOptions": { "rootDir": "src", "baseUrl": ".", - "outDir": "dist", - "esModuleInterop": true, + "outDir": "dist" }, + "include": ["src/**/*"], "exclude": [ - "index.ts", "dist", "node_modules", "test", "**/*.spec.ts", - "**/*.test.ts", - "jest.config.ts" + "**/*.test.ts" ] } \ No newline at end of file diff --git a/packages/contract/.DS_Store b/packages/contract/.DS_Store deleted file mode 100644 index 2af1a89..0000000 Binary files a/packages/contract/.DS_Store and /dev/null differ diff --git a/packages/contract/.gitignore b/packages/contract/.gitignore deleted file mode 100644 index 1c849ee..0000000 --- a/packages/contract/.gitignore +++ /dev/null @@ -1 +0,0 @@ -contract/*.js \ No newline at end of file diff --git a/packages/contract/contract/index.d.ts b/packages/contract/contract/index.d.ts deleted file mode 100644 index 817096b..0000000 --- a/packages/contract/contract/index.d.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { z } from 'zod'; -export declare const CreatePetRequest: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; -}, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; -}, { - name?: string; - status?: "available" | "pending" | "sold"; -}>; -export declare const CreatePetResponse: z.ZodObject<{ - id: z.ZodNumber; -}, "strip", z.ZodTypeAny, { - id?: number; -}, { - id?: number; -}>; -export declare const UpdatePetRequest: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; -}, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; -}, { - name?: string; - status?: "available" | "pending" | "sold"; -}>; -export declare const GetPetResponse: z.ZodObject<{ - id: z.ZodNumber; - name: z.ZodString; - status: z.ZodEnum<["available", "pending", "sold"]>; -}, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; -}, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; -}>; -export declare const ApiContract: { - addPet: { - method: "POST"; - path: string; - description: string; - body: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - }>; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - }, "strip", z.ZodTypeAny, { - id?: number; - }, { - id?: number; - }>; - }; - }; - updatePet: { - method: "PUT"; - path: string; - description: string; - body: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - }>; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - name: z.ZodString; - status: z.ZodEnum<["available", "pending", "sold"]>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }>; - }; - }; - getPetById: { - method: "GET"; - path: string; - description: string; - pathParams: z.ZodObject<{ - petId: z.ZodNumber; - }, "strip", z.ZodTypeAny, { - petId?: number; - }, { - petId?: number; - }>; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - name: z.ZodString; - status: z.ZodEnum<["available", "pending", "sold"]>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }>; - }; - }; -}; -export declare const ApiOperations: { - 'add pet': "addPet"; - 'update pet': "updatePet"; - 'get pet by id': "getPetById"; -}; -export declare const contract: { - addPet: { - description: string; - body: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - }>; - method: "POST"; - path: string; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - }, "strip", z.ZodTypeAny, { - id?: number; - }, { - id?: number; - }>; - }; - }; - updatePet: { - description: string; - body: z.ZodObject<{ - name: z.ZodString; - status: z.ZodOptional>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - }>; - method: "PUT"; - path: string; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - name: z.ZodString; - status: z.ZodEnum<["available", "pending", "sold"]>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }>; - }; - }; - getPetById: { - description: string; - pathParams: z.ZodObject<{ - petId: z.ZodNumber; - }, "strip", z.ZodTypeAny, { - petId?: number; - }, { - petId?: number; - }>; - method: "GET"; - path: string; - responses: { - 200: z.ZodObject<{ - id: z.ZodNumber; - name: z.ZodString; - status: z.ZodEnum<["available", "pending", "sold"]>; - }, "strip", z.ZodTypeAny, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }, { - name?: string; - status?: "available" | "pending" | "sold"; - id?: number; - }>; - }; - }; -}; diff --git a/packages/contract/package.json b/packages/contract/package.json deleted file mode 100644 index 572bb6c..0000000 --- a/packages/contract/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@contractual/contract", - "private": false, - "version": "0.0.0", - "license": "MIT", - "type": "module", - "exports": { - "./contract": { - "import": "./contract/index.js", - "require": "./contract/index.js" - } - }, - "typings": "contract/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/contractual-dev/contractual.git", - "directory": "packages/contract" - }, - "homepage": "https://contractual.dev", - "bugs": { - "url": "https://github.com/contractual-dev/contractual/issues" - }, - "contributors": [ - { - "name": "Omer Morad", - "email": "omer.moradd@gmail.com" - } - ], - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/contractual-dev" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/contractual-dev" - } - ], - "engines": { - "node": ">=18.12.0" - }, - "scripts": { - "prebuild": "pnpm rimraf dist", - "build": "exit 0", - "build:watch": "tsc -p tsconfig.build.json --watch", - "test": "vitest run", - "lint": "exit 0" - }, - "files": [ - "contract", - "dist", - "README.md" - ], - "dependencies": { - "@ts-rest/core": "^3.51.0", - "axios": "^1.7.9", - "zod": "^3.24.1" - }, - "publishConfig": { - "access": "public", - "provenance": true - } -} diff --git a/packages/contract/tsconfig.build.json b/packages/contract/tsconfig.build.json deleted file mode 100644 index 91d6c8b..0000000 --- a/packages/contract/tsconfig.build.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "rootDir": "contract", - "baseUrl": ".", - "outDir": "dist", - "esModuleInterop": true}, - "exclude": [ - "index.ts", - "contract", - "dist", - "node_modules", - "test", - "**/*.spec.ts", - "jest.config.ts" - ] -} \ No newline at end of file diff --git a/packages/contract/tsconfig.json b/packages/contract/tsconfig.json deleted file mode 100644 index 7460ef4..0000000 --- a/packages/contract/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} \ No newline at end of file diff --git a/packages/differs.core/CHANGELOG.md b/packages/differs.core/CHANGELOG.md new file mode 100644 index 0000000..b518735 --- /dev/null +++ b/packages/differs.core/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/differs.core@0.1.0-dev.7...@contractual/differs.core@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* bump rimraf from 5.0.10 to 6.1.3 ([#23](https://github.com/codotech/contractual/issues/23)) ([d8930e9](https://github.com/codotech/contractual/commit/d8930e9bdc69591a5125dc628ec6bbfc94ff4d2b)) +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/differs.core/package.json b/packages/differs.core/package.json new file mode 100644 index 0000000..736fc39 --- /dev/null +++ b/packages/differs.core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@contractual/differs.core", + "version": "0.1.0-beta.0", + "description": "Core schema diffing primitives β€” walker, classifiers, ref-resolver β€” shared across Contractual differs", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/differs.core" + }, + "homepage": "https://github.com/codotech/contractual/tree/next/packages/differs.core", + "bugs": { + "url": "https://github.com/codotech/contractual/issues" + }, + "keywords": [ + "json-schema", + "diff", + "breaking-changes", + "semver", + "schema", + "api", + "compatibility" + ], + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch" + }, + "files": [ + "dist", + "README.md" + ], + "dependencies": { + "@contractual/types": "workspace:*" + }, + "devDependencies": { + "rimraf": "^6.1.3" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/differs.core/src/assemble.ts b/packages/differs.core/src/assemble.ts new file mode 100644 index 0000000..8b113df --- /dev/null +++ b/packages/differs.core/src/assemble.ts @@ -0,0 +1,59 @@ +/** + * Result assembly β€” classify, format, summarize, and compute suggestedBump + */ + +import type { RawChange, Change, ChangeSeverity, DiffResult } from '@contractual/types'; +import { classify, classifyPropertyAdded } from './classifiers.js'; +import { formatChangeMessage } from './format.js'; + +export interface AssembleOptions { + contract?: string; + classifyFn?: (change: RawChange, newSchema: unknown) => ChangeSeverity; +} + +export function assembleResult( + rawChanges: RawChange[], + resolvedNewSchema: unknown, + options?: AssembleOptions +): DiffResult { + const classifyChange = options?.classifyFn ?? defaultClassify; + + const changes: Change[] = rawChanges.map((raw) => ({ + path: raw.path, + severity: classifyChange(raw, resolvedNewSchema), + category: raw.type, + message: formatChangeMessage(raw), + oldValue: raw.oldValue, + newValue: raw.newValue, + })); + + const summary = { + breaking: changes.filter((c) => c.severity === 'breaking').length, + nonBreaking: changes.filter((c) => c.severity === 'non-breaking').length, + patch: changes.filter((c) => c.severity === 'patch').length, + unknown: changes.filter((c) => c.severity === 'unknown').length, + }; + + const suggestedBump = + summary.breaking > 0 + ? 'major' + : summary.nonBreaking > 0 + ? 'minor' + : summary.patch > 0 + ? 'patch' + : 'none'; + + return { + contract: options?.contract ?? '', + changes, + summary, + suggestedBump, + }; +} + +function defaultClassify(change: RawChange, newSchema: unknown): ChangeSeverity { + if (change.type === 'property-added') { + return classifyPropertyAdded(change, newSchema); + } + return classify(change); +} diff --git a/packages/differs.core/src/classifiers.ts b/packages/differs.core/src/classifiers.ts new file mode 100644 index 0000000..59b3930 --- /dev/null +++ b/packages/differs.core/src/classifiers.ts @@ -0,0 +1,417 @@ +/** + * JSON Schema Change Classifiers + * + * Classifies raw structural changes into severity levels for semantic versioning. + * Follows API compatibility principles where breaking changes require major bumps, + * additive changes require minor bumps, and metadata changes are patches. + */ + +import type { RawChange, ChangeType, ChangeSeverity } from '@contractual/types'; + +/** + * Change types that are always breaking (require major version bump) + * + * These changes can break existing consumers: + * - Removing properties removes data they may depend on + * - Adding required fields forces consumers to provide new data + * - Type changes can break parsing/validation + * - Enum removals can invalidate existing data + * - Tightened constraints can reject previously valid data + * - Composition option additions can change validation semantics + * + * Aligned with Strands API classification rules. + */ +const BREAKING_CHANGES: ReadonlySet = new Set([ + 'property-removed', + 'required-added', + 'type-changed', + 'type-narrowed', + 'enum-value-removed', + 'enum-added', + 'constraint-tightened', + 'additional-properties-denied', + 'items-changed', + 'min-items-increased', + 'max-items-decreased', + 'ref-target-changed', + 'dependent-required-added', + // Composition breaking changes (per Strands API) + 'anyof-option-added', + 'oneof-option-added', + 'allof-member-added', + 'not-schema-changed', + // OpenAPI structural breaking changes + 'path-removed', + 'operation-removed', + 'parameter-required-added', + 'parameter-removed', + 'parameter-required-changed', + 'request-body-added', + 'response-removed', + 'security-changed', +]); + +/** + * Change types that are non-breaking (require minor version bump) + * + * These changes are backward compatible additions/relaxations: + * - Adding optional properties extends the schema without breaking + * - Removing required constraints makes the schema more permissive + * - Type widening accepts more values + * - Loosened constraints accept more values + * - Composition option removals make schema less restrictive + * + * Aligned with Strands API classification rules. + */ +const NON_BREAKING_CHANGES: ReadonlySet = new Set([ + 'property-added', + 'required-removed', + 'type-widened', + 'enum-value-added', + 'enum-removed', + 'constraint-loosened', + 'additional-properties-allowed', + 'additional-properties-changed', + 'dependent-required-removed', + // Composition non-breaking changes (per Strands API) + 'anyof-option-removed', + 'oneof-option-removed', + 'allof-member-removed', + // OpenAPI structural non-breaking changes + 'parameter-added', + 'path-added', + 'operation-added', + 'request-body-removed', + 'response-added', + 'server-changed', +]); + +/** + * Change types that are patches (documentation/metadata only) + * + * These changes don't affect validation behavior: + * - Description changes are documentation only + * - Title changes are display metadata + * - Default/example changes don't affect validation + * - Format is an annotation (per Strands API) - doesn't affect validation + * - Annotation keywords (deprecated, readOnly, writeOnly) + * - Content keywords (contentEncoding, contentMediaType, contentSchema) + * + * Aligned with Strands API classification rules. + */ +const PATCH_CHANGES: ReadonlySet = new Set([ + // Metadata changes + 'description-changed', + 'title-changed', + 'default-changed', + 'examples-changed', + // Format is annotation (patch per Strands API) + 'format-added', + 'format-removed', + 'format-changed', + // Annotation keywords (Draft 2019-09+) + 'deprecated-changed', + 'read-only-changed', + 'write-only-changed', + // Content keywords + 'content-encoding-changed', + 'content-media-type-changed', + 'content-schema-changed', +]); + +/** + * Change types that require manual review + * + * These changes are too complex to classify automatically: + * - Generic composition changes require semantic analysis + * - Complex keywords (propertyNames, dependentSchemas, unevaluated*) + * - Conditional schema changes (if/then/else) + * - Unknown changes need human evaluation + * + * Aligned with Strands API classification rules. + */ +const UNKNOWN_CHANGES: ReadonlySet = new Set([ + // Complex object keywords + 'property-names-changed', + 'dependent-schemas-changed', + 'unevaluated-properties-changed', + // Complex array keywords + 'unevaluated-items-changed', + 'min-contains-changed', + 'max-contains-changed', + // Conditional schema + 'if-then-else-changed', + // Legacy/generic composition + 'composition-changed', + // OpenAPI context-dependent (schema-level changes handled by walker) + 'parameter-schema-changed', + 'response-schema-changed', + // Catch-all + 'unknown-change', +]); + +/** + * Export classification sets for external analysis + */ +export const CLASSIFICATION_SETS = { + breaking: BREAKING_CHANGES, + nonBreaking: NON_BREAKING_CHANGES, + patch: PATCH_CHANGES, + unknown: UNKNOWN_CHANGES, +} as const; + +/** + * Classify a raw change into a severity level + * + * Uses the Strands API classification rules where: + * - Breaking changes require major version bump + * - Non-breaking changes require minor version bump + * - Patch changes are metadata/annotation only + * - Unknown changes require manual review + * + * @param change - The raw change to classify + * @returns The severity classification + * + * @example + * ```typescript + * const change: RawChange = { + * path: '/properties/name', + * type: 'property-removed', + * oldValue: { type: 'string' }, + * }; + * const severity = classify(change); + * // severity === 'breaking' + * ``` + */ +export function classify(change: RawChange): ChangeSeverity { + const { type } = change; + + // Check each category in order of specificity + if (BREAKING_CHANGES.has(type)) { + return 'breaking'; + } + + if (NON_BREAKING_CHANGES.has(type)) { + return 'non-breaking'; + } + + if (PATCH_CHANGES.has(type)) { + return 'patch'; + } + + if (UNKNOWN_CHANGES.has(type)) { + return 'unknown'; + } + + // Defensive: any unhandled type is unknown + return 'unknown'; +} + +/** + * Classify a property-added change with schema context + * + * Property additions are breaking if the property is required, + * otherwise they are non-breaking (additive). + * + * @param change - The property-added change + * @param newSchema - The new schema for context (to check required array) + * @returns The severity classification + * + * @example + * ```typescript + * const change: RawChange = { + * path: '/properties/email', + * type: 'property-added', + * newValue: { type: 'string', format: 'email' }, + * }; + * + * const schema = { + * type: 'object', + * properties: { email: { type: 'string', format: 'email' } }, + * required: ['email'], // email is required! + * }; + * + * const severity = classifyPropertyAdded(change, schema); + * // severity === 'breaking' (because email is in required[]) + * ``` + */ +export function classifyPropertyAdded(change: RawChange, newSchema: unknown): ChangeSeverity { + // Validate change type + if (change.type !== 'property-added') { + return classify(change); + } + + // Extract property name from path + const propertyName = extractPropertyName(change.path); + if (!propertyName) { + // Cannot determine property name, fall back to non-breaking + return 'non-breaking'; + } + + // Find the parent schema containing this property + const parentSchema = findParentSchema(change.path, newSchema); + if (!parentSchema) { + // Cannot find parent schema, fall back to non-breaking + return 'non-breaking'; + } + + // Check if property is in the required array + const required = getRequiredArray(parentSchema); + if (required.includes(propertyName)) { + // Adding a required property is breaking + return 'breaking'; + } + + // Adding an optional property is non-breaking + return 'non-breaking'; +} + +/** + * Extract the property name from a JSON Pointer path + * + * @param path - JSON Pointer path (e.g., '/properties/name' or '/properties/user/properties/email') + * @returns The property name or null if not found + */ +function extractPropertyName(path: string): string | null { + // Match the last /properties/NAME segment + const match = path.match(/\/properties\/([^/]+)$/); + if (match?.[1] !== undefined) { + return decodeJsonPointerSegment(match[1]); + } + + return null; +} + +/** + * Decode a JSON Pointer segment (handles ~0 and ~1 escapes) + * + * @param segment - The encoded segment + * @returns The decoded segment + */ +function decodeJsonPointerSegment(segment: string): string { + return segment.replace(/~1/g, '/').replace(/~0/g, '~'); +} + +/** + * Find the parent schema containing a property + * + * @param path - JSON Pointer path to the property + * @param schema - The root schema + * @returns The parent schema or null if not found + */ +function findParentSchema(path: string, schema: unknown): unknown { + if (!isObject(schema)) { + return null; + } + + // Remove the last segment to get parent path + // e.g., '/properties/name' -> '' (root) + // e.g., '/properties/user/properties/email' -> '/properties/user' + const segments = path.split('/').filter(Boolean); + + // We need to navigate to the schema containing /properties/NAME + // So we remove 'properties' and 'NAME' from the end + if (segments.length < 2) { + // Path is too short, parent is root + return schema; + } + + // Remove 'NAME' and 'properties' from the end + const parentSegments = segments.slice(0, -2); + + // Navigate to parent + let current: unknown = schema; + for (const segment of parentSegments) { + if (!isObject(current)) { + return null; + } + const decoded = decodeJsonPointerSegment(segment); + current = (current as Record)[decoded]; + } + + return current; +} + +/** + * Get the required array from a schema object + * + * @param schema - The schema object + * @returns Array of required property names + */ +function getRequiredArray(schema: unknown): string[] { + if (!isObject(schema)) { + return []; + } + + const obj = schema as Record; + const required = obj['required']; + + if (!Array.isArray(required)) { + return []; + } + + return required.filter((item): item is string => typeof item === 'string'); +} + +/** + * Type guard for objects + */ +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Batch classify multiple changes + * + * @param changes - Array of raw changes + * @param newSchema - Optional schema for context-aware classification + * @returns Map of change to severity + */ +export function classifyAll( + changes: readonly RawChange[], + newSchema?: unknown +): Map { + const results = new Map(); + + for (const change of changes) { + if (change.type === 'property-added' && newSchema !== undefined) { + results.set(change, classifyPropertyAdded(change, newSchema)); + } else { + results.set(change, classify(change)); + } + } + + return results; +} + +/** + * Get a human-readable message for a classified change + * + * @param change - The raw change + * @param severity - The classified severity + * @returns Human-readable description + */ +export function getChangeMessage(change: RawChange, severity: ChangeSeverity): string { + const severityLabel = + severity === 'breaking' + ? 'BREAKING' + : severity === 'non-breaking' + ? 'Non-breaking' + : severity === 'patch' + ? 'Patch' + : 'Unknown'; + + const typeLabel = formatChangeType(change.type); + + return `[${severityLabel}] ${typeLabel} at ${change.path}`; +} + +/** + * Format a change type into a human-readable label + */ +function formatChangeType(type: ChangeType): string { + return type + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} diff --git a/packages/differs.core/src/format.ts b/packages/differs.core/src/format.ts new file mode 100644 index 0000000..111b1f3 --- /dev/null +++ b/packages/differs.core/src/format.ts @@ -0,0 +1,274 @@ +/** + * Human-readable message formatting for raw changes + */ + +import type { RawChange } from '@contractual/types'; + +/** + * Format a human-readable message for a change + * + * @param change - The raw change to format + * @returns Human-readable message describing the change + */ +export function formatChangeMessage(change: RawChange): string { + const pathDisplay = decodeJsonPointer(change.path || '/'); + + switch (change.type) { + case 'property-added': + return `Property added at ${pathDisplay}`; + + case 'property-removed': + return `Property removed at ${pathDisplay}`; + + case 'required-added': + return `Field "${formatRequiredFieldName(change.newValue)}" made required at ${pathDisplay}`; + + case 'required-removed': + return `Field "${formatRequiredFieldName(change.oldValue)}" made optional at ${pathDisplay}`; + + case 'type-changed': + return `Type changed from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'type-narrowed': + return `Type narrowed from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'type-widened': + return `Type widened from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'enum-value-added': + return `Enum value ${formatValue(change.newValue)} added at ${pathDisplay}`; + + case 'enum-value-removed': + return `Enum value ${formatValue(change.oldValue)} removed at ${pathDisplay}`; + + case 'enum-added': + return `Enum constraint added at ${pathDisplay}`; + + case 'enum-removed': + return `Enum constraint removed at ${pathDisplay}`; + + case 'constraint-tightened': + return formatConstraintMessage(change, 'tightened', pathDisplay); + + case 'constraint-loosened': + return formatConstraintMessage(change, 'loosened', pathDisplay); + + case 'format-added': + return `Format "${change.newValue}" added at ${pathDisplay}`; + + case 'format-removed': + return `Format "${change.oldValue}" removed at ${pathDisplay}`; + + case 'format-changed': + return `Format changed from "${change.oldValue}" to "${change.newValue}" at ${pathDisplay}`; + + case 'additional-properties-denied': + return `Additional properties denied at ${pathDisplay}`; + + case 'additional-properties-allowed': + return `Additional properties allowed at ${pathDisplay}`; + + case 'additional-properties-changed': + return `Additional properties schema changed at ${pathDisplay}`; + + case 'items-changed': + return `Array items schema changed at ${pathDisplay}`; + + case 'min-items-increased': + return `Minimum items increased from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'max-items-decreased': + return `Maximum items decreased from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'composition-changed': + return `Composition (allOf/anyOf/oneOf) changed at ${pathDisplay}`; + + case 'ref-target-changed': + return `Reference target changed at ${pathDisplay}`; + + case 'description-changed': + return `Description changed at ${pathDisplay}`; + + case 'title-changed': + return `Title changed at ${pathDisplay}`; + + case 'default-changed': + return `Default value changed at ${pathDisplay}`; + + case 'examples-changed': + return `Examples changed at ${pathDisplay}`; + + case 'deprecated-changed': + return change.newValue + ? `Deprecated at ${pathDisplay}` + : `No longer deprecated at ${pathDisplay}`; + + case 'read-only-changed': + return `readOnly changed at ${pathDisplay}`; + + case 'write-only-changed': + return `writeOnly changed at ${pathDisplay}`; + + // OpenAPI structural changes + case 'path-added': + return `New path added: ${change.newValue ?? pathDisplay}`; + + case 'path-removed': + return `Path removed: ${change.oldValue ?? pathDisplay}`; + + case 'operation-added': + return `New operation added: ${change.newValue ?? pathDisplay}`; + + case 'operation-removed': + return `Operation removed: ${change.oldValue ?? pathDisplay}`; + + case 'parameter-added': + return `Optional parameter added at ${pathDisplay}`; + + case 'parameter-required-added': + return `Required parameter added at ${pathDisplay}`; + + case 'parameter-removed': + return `Parameter removed at ${pathDisplay}`; + + case 'parameter-required-changed': + return `Parameter required changed from ${formatValue(change.oldValue)} to ${formatValue(change.newValue)} at ${pathDisplay}`; + + case 'parameter-schema-changed': + return `Parameter schema changed at ${pathDisplay}`; + + case 'request-body-added': + return `Request body added at ${pathDisplay}`; + + case 'request-body-removed': + return `Request body removed at ${pathDisplay}`; + + case 'response-added': + return `Response ${formatValue(change.newValue)} added at ${pathDisplay}`; + + case 'response-removed': + return `Response ${formatValue(change.oldValue)} removed at ${pathDisplay}`; + + case 'response-schema-changed': + return `Response schema changed at ${pathDisplay}`; + + case 'security-changed': + return `Security changed at ${pathDisplay}`; + + case 'server-changed': + return `Server changed at ${pathDisplay}`; + + // Composition changes + case 'anyof-option-added': + return `anyOf option added at ${pathDisplay}`; + + case 'anyof-option-removed': + return `anyOf option removed at ${pathDisplay}`; + + case 'oneof-option-added': + return `oneOf option added at ${pathDisplay}`; + + case 'oneof-option-removed': + return `oneOf option removed at ${pathDisplay}`; + + case 'allof-member-added': + return `allOf member added at ${pathDisplay}`; + + case 'allof-member-removed': + return `allOf member removed at ${pathDisplay}`; + + case 'not-schema-changed': + return `not schema changed at ${pathDisplay}`; + + case 'if-then-else-changed': + return `Conditional schema (if/then/else) changed at ${pathDisplay}`; + + case 'unknown-change': + default: + return `Unknown change at ${pathDisplay}`; + } +} + +/** + * Decode a JSON Pointer path for human-readable display (RFC 6901) + * Decodes each segment individually so that slashes within keys + * (e.g. OpenAPI path keys like /v1/extract) are kept intact + * rather than being confused with segment separators. + */ +function decodeJsonPointer(path: string): string { + if (!path.startsWith('/')) return path; + + const segments = path.split('/'); + // segments[0] is '' (before the leading /) + const decoded = segments.map((seg, i) => { + if (i === 0) return seg; + const value = seg.replace(/~1/g, '/').replace(/~0/g, '~'); + if (value.includes('/')) return `[${value}]`; + return value; + }); + + return decoded.join('/'); +} + +/** + * Format a value for display in messages + */ +function formatValue(value: unknown): string { + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + if (typeof value === 'string') { + return `"${value}"`; + } + if (Array.isArray(value)) { + return JSON.stringify(value); + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); +} + +/** + * Format required field name from change value + */ +function formatRequiredFieldName(value: unknown): string { + if (typeof value === 'string') { + return value; + } + return String(value); +} + +/** + * Format constraint change message + */ +function formatConstraintMessage( + change: RawChange, + direction: 'tightened' | 'loosened', + pathDisplay: string +): string { + const constraintName = extractConstraintName(change.path); + const oldVal = change.oldValue !== undefined ? formatValue(change.oldValue) : 'none'; + const newVal = change.newValue !== undefined ? formatValue(change.newValue) : 'none'; + + if (constraintName) { + return `Constraint "${constraintName}" ${direction} from ${oldVal} to ${newVal} at ${pathDisplay}`; + } + + return `Constraint ${direction} from ${oldVal} to ${newVal} at ${pathDisplay}`; +} + +/** + * Extract constraint name from path + */ +function extractConstraintName(path: string): string | null { + const segments = path.split('/'); + const lastSegment = segments[segments.length - 1]; + if (lastSegment && lastSegment !== '') { + return lastSegment; + } + return null; +} diff --git a/packages/differs.core/src/index.ts b/packages/differs.core/src/index.ts new file mode 100644 index 0000000..bbb5561 --- /dev/null +++ b/packages/differs.core/src/index.ts @@ -0,0 +1,50 @@ +// Core walker +export { walk } from './walker.js'; + +// Classification +export { classify, classifyPropertyAdded, classifyAll, CLASSIFICATION_SETS } from './classifiers.js'; + +// Ref resolution +export { resolveRefs, hasUnresolvedRefs, extractRefs, validateRefs, type ResolveResult } from './ref-resolver.js'; + +// Message formatting +export { formatChangeMessage } from './format.js'; + +// Result assembly +export { assembleResult, type AssembleOptions } from './assemble.js'; + +// Schema types and utilities +export type { + ResolvedSchema, + JSONSchemaType, + NormalizedType, + ConstraintKey, + ConstraintDirection, + ConstraintMeta, + CompositionKeyword, + MetadataKey, + AnnotationKey, + ContentKey, + WalkerContext, +} from './types.js'; + +export { + isSchemaObject, + isSchemaArray, + normalizeType, + arraysEqual, + deepEqual, + escapeJsonPointer, + joinPath, + CONSTRAINT_KEYS, + CONSTRAINT_DIRECTION, + COMPOSITION_KEYWORDS, + METADATA_KEYS, + ANNOTATION_KEYS, + CONTENT_KEYS, + DEFAULT_MAX_DEPTH, +} from './types.js'; + +// Re-export governance types from @contractual/types for convenience +export type { ChangeType, ChangeSeverity, RawChange, Change, DiffResult, DiffSummary, SuggestedBump } from '@contractual/types'; +export { CHANGE_TYPE_SEVERITY } from '@contractual/types'; diff --git a/packages/differs.core/src/ref-resolver.ts b/packages/differs.core/src/ref-resolver.ts new file mode 100644 index 0000000..c125601 --- /dev/null +++ b/packages/differs.core/src/ref-resolver.ts @@ -0,0 +1,435 @@ +/** + * JSON Schema $ref Resolver + * + * Resolves all $ref pointers in a JSON Schema, producing a self-contained schema. + * Handles internal references ($defs, definitions) and detects circular references. + * + * This resolver operates on in-memory schema objects and does not fetch external URLs. + * External references are flagged as warnings. + */ + +/** + * Result of resolving references in a schema + */ +export interface ResolveResult { + /** The resolved schema with $refs replaced */ + readonly schema: unknown; + /** Warnings encountered during resolution (circular refs, external refs, etc.) */ + readonly warnings: string[]; +} + +/** + * Internal context for tracking resolution state + */ +interface ResolveContext { + /** The root schema for resolving internal references */ + readonly root: unknown; + /** Accumulated warnings */ + readonly warnings: string[]; + /** Set of $ref paths currently being resolved (for circular detection) */ + readonly resolving: Set; + /** Cache of already resolved $refs to their values */ + readonly cache: Map; + /** Current JSON Pointer path (for error messages) */ + currentPath: string; +} + +/** + * Resolve all $ref pointers in a JSON Schema + * + * Creates a self-contained schema by inlining all internal references. + * Does not modify the original schema. + * + * @param schema - The JSON Schema to resolve + * @returns The resolved schema and any warnings + * + * @example + * ```typescript + * const schema = { + * type: 'object', + * properties: { + * user: { $ref: '#/$defs/User' } + * }, + * $defs: { + * User: { type: 'object', properties: { name: { type: 'string' } } } + * } + * }; + * + * const result = resolveRefs(schema); + * // result.schema.properties.user === { type: 'object', properties: { name: { type: 'string' } } } + * // result.warnings === [] + * ``` + */ +export function resolveRefs(schema: unknown): ResolveResult { + const context = { + root: schema, + warnings: [], + resolving: new Set(), + cache: new Map(), + currentPath: '', + } satisfies ResolveContext; + + const resolved = resolveNode(schema, context); + + return { + schema: resolved, + warnings: context.warnings, + }; +} + +/** + * Recursively resolve a schema node + */ +function resolveNode(node: unknown, context: ResolveContext): unknown { + // Handle non-objects + if (!isObject(node)) { + return node; + } + + const obj = node as Record; + + // Check if this is a $ref node + if (typeof obj['$ref'] === 'string') { + return resolveRef(obj['$ref'], obj, context); + } + + // Recursively resolve all properties + const resolved: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + const previousPath = context.currentPath; + context.currentPath = `${context.currentPath}/${encodeJsonPointerSegment(key)}`; + + if (Array.isArray(value)) { + resolved[key] = value.map((item, index) => { + const itemPath = context.currentPath; + context.currentPath = `${itemPath}/${index}`; + const resolvedItem = resolveNode(item, context); + context.currentPath = itemPath; + return resolvedItem; + }); + } else { + resolved[key] = resolveNode(value, context); + } + + context.currentPath = previousPath; + } + + return resolved; +} + +/** + * Resolve a $ref pointer + */ +function resolveRef( + ref: string, + refNode: Record, + context: ResolveContext +): unknown { + // Check for external references + if (isExternalRef(ref)) { + context.warnings.push( + `External reference not resolved: ${ref} at ${context.currentPath || '/'}` + ); + // Return the $ref node as-is for external refs + return refNode; + } + + // Check for circular reference + if (context.resolving.has(ref)) { + context.warnings.push(`Circular reference detected: ${ref} at ${context.currentPath || '/'}`); + // Return a placeholder to break the cycle + return { + $circularRef: ref, + $comment: 'Circular reference - see original location', + }; + } + + // Check cache + if (context.cache.has(ref)) { + return context.cache.get(ref); + } + + // Mark as currently resolving + context.resolving.add(ref); + + try { + // Resolve the reference + const target = resolvePointer(ref, context.root); + + if (target === undefined) { + context.warnings.push(`Reference not found: ${ref} at ${context.currentPath || '/'}`); + // Return the $ref node as-is if target not found + return refNode; + } + + // Recursively resolve the target (it may contain more $refs) + const resolved = resolveNode(target, context); + + // Merge any sibling properties from the $ref node + // JSON Schema allows properties alongside $ref in draft 2019-09+ + const siblings = extractSiblingProperties(refNode); + const merged = mergeSiblings(resolved, siblings); + + // Cache the result + context.cache.set(ref, merged); + + return merged; + } finally { + // Remove from resolving set + context.resolving.delete(ref); + } +} + +/** + * Check if a reference is external (http://, https://, file://) + */ +function isExternalRef(ref: string): boolean { + return ( + ref.startsWith('http://') || + ref.startsWith('https://') || + ref.startsWith('file://') || + // Relative file references (not starting with #) + (!ref.startsWith('#') && + (ref.endsWith('.json') || ref.endsWith('.yaml') || ref.endsWith('.yml'))) + ); +} + +/** + * Resolve a JSON Pointer within a document + * + * Supports: + * - #/$defs/Name + * - #/definitions/Name + * - #/properties/field/items + * + * @param pointer - The JSON Pointer (e.g., '#/$defs/User') + * @param root - The root document + * @returns The resolved value or undefined if not found + */ +function resolvePointer(pointer: string, root: unknown): unknown { + // Handle empty or root pointer + if (pointer === '#' || pointer === '') { + return root; + } + + // Remove the leading # if present + let path = pointer; + if (path.startsWith('#')) { + path = path.slice(1); + } + + // Remove leading slash + if (path.startsWith('/')) { + path = path.slice(1); + } + + // Handle empty path after normalization + if (!path) { + return root; + } + + // Split into segments and navigate + const segments = path.split('/'); + let current: unknown = root; + + for (const segment of segments) { + if (!isObject(current) && !Array.isArray(current)) { + return undefined; + } + + const decoded = decodeJsonPointerSegment(segment); + + if (Array.isArray(current)) { + const index = parseInt(decoded, 10); + if (isNaN(index) || index < 0 || index >= current.length) { + return undefined; + } + current = current[index]; + } else { + const obj = current as Record; + if (!(decoded in obj)) { + return undefined; + } + current = obj[decoded]; + } + } + + return current; +} + +/** + * Decode a JSON Pointer segment (RFC 6901) + * + * ~0 -> ~ + * ~1 -> / + */ +function decodeJsonPointerSegment(segment: string): string { + return segment.replace(/~1/g, '/').replace(/~0/g, '~'); +} + +/** + * Encode a JSON Pointer segment (RFC 6901) + * + * ~ -> ~0 + * / -> ~1 + */ +function encodeJsonPointerSegment(segment: string): string { + return segment.replace(/~/g, '~0').replace(/\//g, '~1'); +} + +/** + * Extract sibling properties from a $ref node + * + * In JSON Schema draft 2019-09+, properties alongside $ref are allowed + * and should be merged with the referenced schema. + */ +function extractSiblingProperties(refNode: Record): Record { + const siblings: Record = {}; + + for (const [key, value] of Object.entries(refNode)) { + if (key !== '$ref') { + siblings[key] = value; + } + } + + return siblings; +} + +/** + * Merge sibling properties with a resolved schema + */ +function mergeSiblings(resolved: unknown, siblings: Record): unknown { + // If no siblings, return as-is + if (Object.keys(siblings).length === 0) { + return resolved; + } + + // If resolved is not an object, wrap in allOf + if (!isObject(resolved)) { + return { + allOf: [resolved, siblings], + }; + } + + // Merge properties, siblings override resolved + return { + ...(resolved as Record), + ...siblings, + }; +} + +/** + * Type guard for objects + */ +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Check if a schema contains any unresolved $refs + * + * @param schema - The schema to check + * @returns True if the schema contains $ref pointers + */ +export function hasUnresolvedRefs(schema: unknown): boolean { + if (!isObject(schema)) { + return false; + } + + const obj = schema as Record; + + // Check if this node is a $ref + if (typeof obj['$ref'] === 'string' && !obj['$circularRef']) { + return true; + } + + // Check children + for (const value of Object.values(obj)) { + if (Array.isArray(value)) { + for (const item of value) { + if (hasUnresolvedRefs(item)) { + return true; + } + } + } else if (hasUnresolvedRefs(value)) { + return true; + } + } + + return false; +} + +/** + * Extract all $ref pointers from a schema + * + * @param schema - The schema to analyze + * @returns Array of { pointer, path } objects + */ +export function extractRefs(schema: unknown): Array<{ pointer: string; path: string }> { + const refs: Array<{ pointer: string; path: string }> = []; + collectRefs(schema, '', refs); + return refs; +} + +/** + * Recursively collect $refs + */ +function collectRefs( + node: unknown, + path: string, + refs: Array<{ pointer: string; path: string }> +): void { + if (!isObject(node)) { + return; + } + + const obj = node as Record; + + if (typeof obj['$ref'] === 'string') { + refs.push({ pointer: obj['$ref'], path: path || '/' }); + } + + for (const [key, value] of Object.entries(obj)) { + const childPath = `${path}/${encodeJsonPointerSegment(key)}`; + + if (Array.isArray(value)) { + value.forEach((item, index) => { + collectRefs(item, `${childPath}/${index}`, refs); + }); + } else { + collectRefs(value, childPath, refs); + } + } +} + +/** + * Validate that all internal references in a schema are resolvable + * + * @param schema - The schema to validate + * @returns Object with valid flag and any error messages + */ +export function validateRefs(schema: unknown): { + valid: boolean; + errors: string[]; +} { + const errors: string[] = []; + const refs = extractRefs(schema); + + for (const { pointer, path } of refs) { + // Skip external refs for this validation + if (isExternalRef(pointer)) { + continue; + } + + const resolved = resolvePointer(pointer, schema); + if (resolved === undefined) { + errors.push(`Invalid reference at ${path}: ${pointer} not found`); + } + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/packages/differs.core/src/types.ts b/packages/differs.core/src/types.ts new file mode 100644 index 0000000..c907e57 --- /dev/null +++ b/packages/differs.core/src/types.ts @@ -0,0 +1,347 @@ +/** + * Core schema types and utilities for Contractual differs + */ + +// Re-export governance types from @contractual/types +export type { + ChangeType, + ChangeSeverity, + RawChange, + Change, + DiffResult, + DiffSummary, + SuggestedBump, +} from '@contractual/types'; + +export { CHANGE_TYPE_SEVERITY } from '@contractual/types'; + +// ============================================================================ +// JSON Schema Types +// ============================================================================ + +/** + * JSON Schema type values + */ +export type JSONSchemaType = + | 'string' + | 'number' + | 'integer' + | 'boolean' + | 'object' + | 'array' + | 'null'; + +/** + * Normalized type representation (always an array for comparison) + */ +export type NormalizedType = JSONSchemaType[]; + +/** + * JSON Schema constraint keys that can be tightened or loosened + */ +export type ConstraintKey = + | 'minimum' + | 'maximum' + | 'exclusiveMinimum' + | 'exclusiveMaximum' + | 'minLength' + | 'maxLength' + | 'minItems' + | 'maxItems' + | 'minProperties' + | 'maxProperties' + | 'minContains' + | 'maxContains' + | 'pattern' + | 'multipleOf' + | 'uniqueItems'; + +/** + * Constraint comparison direction + */ +export type ConstraintDirection = 'min' | 'max' | 'exact'; + +/** + * Constraint metadata for determining tightened vs loosened + */ +export interface ConstraintMeta { + readonly key: ConstraintKey; + readonly direction: ConstraintDirection; +} + +/** + * Map of constraint keys to their comparison direction + */ +export const CONSTRAINT_DIRECTION: Record = { + minimum: 'min', + maximum: 'max', + exclusiveMinimum: 'min', + exclusiveMaximum: 'max', + minLength: 'min', + maxLength: 'max', + minItems: 'min', + maxItems: 'max', + minProperties: 'min', + maxProperties: 'max', + minContains: 'min', + maxContains: 'max', + pattern: 'exact', + multipleOf: 'exact', + uniqueItems: 'exact', +}; + +/** + * All constraint keys for iteration + */ +export const CONSTRAINT_KEYS: ConstraintKey[] = [ + 'minimum', + 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', + 'minLength', + 'maxLength', + 'minItems', + 'maxItems', + 'minProperties', + 'maxProperties', + 'minContains', + 'maxContains', + 'pattern', + 'multipleOf', + 'uniqueItems', +]; + +/** + * Composition keywords in JSON Schema + */ +export type CompositionKeyword = 'anyOf' | 'oneOf' | 'allOf' | 'if' | 'then' | 'else' | 'not'; + +/** + * All composition keywords for iteration + */ +export const COMPOSITION_KEYWORDS: CompositionKeyword[] = [ + 'anyOf', + 'oneOf', + 'allOf', + 'if', + 'then', + 'else', + 'not', +]; + +/** + * Metadata keys that are compared (patch-level changes) + */ +export type MetadataKey = 'description' | 'title' | 'default' | 'examples'; + +/** + * All metadata keys for iteration + */ +export const METADATA_KEYS: MetadataKey[] = ['description', 'title', 'default', 'examples']; + +/** + * Annotation keys (patch-level changes per Strands API) + */ +export type AnnotationKey = 'deprecated' | 'readOnly' | 'writeOnly'; + +/** + * All annotation keys for iteration + */ +export const ANNOTATION_KEYS: AnnotationKey[] = ['deprecated', 'readOnly', 'writeOnly']; + +/** + * Content keywords (patch-level changes per Strands API) + */ +export type ContentKey = 'contentEncoding' | 'contentMediaType' | 'contentSchema'; + +/** + * All content keys for iteration + */ +export const CONTENT_KEYS: ContentKey[] = ['contentEncoding', 'contentMediaType', 'contentSchema']; + +/** + * Resolved JSON Schema object (refs already resolved) + */ +export interface ResolvedSchema { + // Schema identification + $schema?: string; + $id?: string; + $ref?: string; + $defs?: Record; + + // Type + type?: JSONSchemaType | JSONSchemaType[]; + + // Metadata + title?: string; + description?: string; + default?: unknown; + examples?: unknown[]; + + // Annotations (Draft 2019-09+) + deprecated?: boolean; + readOnly?: boolean; + writeOnly?: boolean; + + // Enum + enum?: unknown[]; + const?: unknown; + + // Format + format?: string; + + // Numeric constraints + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; + + // String constraints + minLength?: number; + maxLength?: number; + pattern?: string; + + // Content keywords (Draft 7+) + contentEncoding?: string; + contentMediaType?: string; + contentSchema?: ResolvedSchema; + + // Object keywords + properties?: Record; + required?: string[]; + additionalProperties?: boolean | ResolvedSchema; + minProperties?: number; + maxProperties?: number; + propertyNames?: ResolvedSchema; + patternProperties?: Record; + dependentRequired?: Record; + dependentSchemas?: Record; + unevaluatedProperties?: boolean | ResolvedSchema; + + // Array keywords + items?: ResolvedSchema | ResolvedSchema[]; + prefixItems?: ResolvedSchema[]; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + contains?: ResolvedSchema; + minContains?: number; + maxContains?: number; + unevaluatedItems?: boolean | ResolvedSchema; + + // Composition + anyOf?: ResolvedSchema[]; + oneOf?: ResolvedSchema[]; + allOf?: ResolvedSchema[]; + if?: ResolvedSchema; + then?: ResolvedSchema; + else?: ResolvedSchema; + not?: ResolvedSchema; + + // Allow additional properties for extensibility + [key: string]: unknown; +} + +/** + * Walker context for tracking traversal state + */ +export interface WalkerContext { + /** Current JSON Pointer path */ + readonly path: string; + /** Depth of recursion (for cycle detection) */ + readonly depth: number; + /** Maximum allowed depth */ + readonly maxDepth: number; +} + +/** + * Default maximum recursion depth + */ +export const DEFAULT_MAX_DEPTH = 100; + +/** + * Type guard to check if value is a schema object + */ +export function isSchemaObject(value: unknown): value is ResolvedSchema { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Type guard to check if value is an array of schemas + */ +export function isSchemaArray(value: unknown): value is ResolvedSchema[] { + return Array.isArray(value) && value.every(isSchemaObject); +} + +/** + * Normalize type to array for consistent comparison + */ +export function normalizeType(type: JSONSchemaType | JSONSchemaType[] | undefined): NormalizedType { + if (type === undefined) { + return []; + } + if (Array.isArray(type)) { + return [...type].sort(); + } + return [type]; +} + +/** + * Check if two arrays have the same elements (order-independent) + */ +export function arraysEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); + return sortedA.every((val, idx) => val === sortedB[idx]); +} + +/** + * Deep equality check for JSON values + */ +export function deepEqual(a: unknown, b: unknown): boolean { + if (a === b) { + return true; + } + + if (typeof a !== typeof b) { + return false; + } + + if (a === null || b === null) return a === b; + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((val, idx) => deepEqual(val, b[idx])); + } + + if (typeof a === 'object' && typeof b === 'object') { + const aObj = a as Record; + const bObj = b as Record; + const aKeys = Object.keys(aObj); + const bKeys = Object.keys(bObj); + if (aKeys.length !== bKeys.length) return false; + return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); + } + + return false; +} + +/** + * Escape JSON Pointer segment according to RFC 6901 + */ +export function escapeJsonPointer(segment: string): string { + return segment.replace(/~/g, '~0').replace(/\//g, '~1'); +} + +/** + * Join path segments into a JSON Pointer + */ +export function joinPath(basePath: string, ...segments: string[]): string { + const escaped = segments.map(escapeJsonPointer); + if (basePath === '') { + return escaped.length > 0 ? '/' + escaped.join('/') : ''; + } + return basePath + '/' + escaped.join('/'); +} diff --git a/packages/differs.core/src/walker.ts b/packages/differs.core/src/walker.ts new file mode 100644 index 0000000..e1d47db --- /dev/null +++ b/packages/differs.core/src/walker.ts @@ -0,0 +1,1578 @@ +/** + * JSON Schema structural walker + * + * Recursively walks two resolved JSON Schemas side-by-side (DFS) + * and emits RawChange for every structural difference. + */ + +import type { ChangeType, RawChange, CONTENT_KEYS } from './types.js'; +import { + ANNOTATION_KEYS, + arraysEqual, + CONSTRAINT_DIRECTION, + CONSTRAINT_KEYS, + type ConstraintKey, + deepEqual, + DEFAULT_MAX_DEPTH, + isSchemaObject, + joinPath, + METADATA_KEYS, + type NormalizedType, + normalizeType, + type ResolvedSchema, +} from './types.js'; + +/** + * Walk two resolved JSON Schemas and emit changes + * + * @param oldSchema - The original schema (resolved, no $refs) + * @param newSchema - The new schema (resolved, no $refs) + * @param basePath - JSON Pointer base path (default: '') + * @returns Array of raw changes detected + */ +export function walk(oldSchema: unknown, newSchema: unknown, basePath: string = ''): RawChange[] { + return walkInternal(oldSchema, newSchema, basePath, 0); +} + +/** + * Internal walk function with depth tracking + */ +function walkInternal( + oldSchema: unknown, + newSchema: unknown, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + // Prevent infinite recursion + if (depth > DEFAULT_MAX_DEPTH) { + return changes; + } + + // Handle null/undefined schemas + if (oldSchema === undefined && newSchema === undefined) { + return changes; + } + + // Normalize to objects for comparison + const oldObj = isSchemaObject(oldSchema) ? oldSchema : null; + const newObj = isSchemaObject(newSchema) ? newSchema : null; + + // Schema added or removed entirely + if (oldObj === null && newObj !== null) { + changes.push({ + path, + type: 'property-added', + oldValue: undefined, + newValue: newSchema, + }); + return changes; + } + + if (oldObj !== null && newObj === null) { + changes.push({ + path, + type: 'property-removed', + oldValue: oldSchema, + newValue: undefined, + }); + return changes; + } + + // Both are non-object (boolean schemas in JSON Schema draft-06+) + if (oldObj === null && newObj === null) { + if (oldSchema !== newSchema) { + changes.push({ + path, + type: 'unknown-change', + oldValue: oldSchema, + newValue: newSchema, + }); + } + return changes; + } + + // Both are schema objects - compare all aspects + const old = oldObj as ResolvedSchema; + const newS = newObj as ResolvedSchema; + + // 1. Metadata changes (title, description, default, examples) + changes.push(...compareMetadata(old, newS, path)); + + // 2. Annotation changes (deprecated, readOnly, writeOnly) + changes.push(...compareAnnotations(old, newS, path)); + + // 3. Content keyword changes (contentEncoding, contentMediaType, contentSchema) + changes.push(...compareContentKeywords(old, newS, path, depth)); + + // 4. Type changes + changes.push(...compareType(old, newS, path)); + + // 5. Enum changes + changes.push(...compareEnum(old, newS, path)); + + // 6. Format changes + changes.push(...compareFormat(old, newS, path)); + + // 7. Constraint changes + changes.push(...compareConstraints(old, newS, path)); + + // 8. Properties changes (recurse) + changes.push(...compareProperties(old, newS, path, depth)); + + // 9. Required changes + changes.push(...compareRequired(old, newS, path)); + + // 10. additionalProperties changes + changes.push(...compareAdditionalProperties(old, newS, path, depth)); + + // 11. propertyNames changes + changes.push(...comparePropertyNames(old, newS, path, depth)); + + // 12. dependentRequired changes + changes.push(...compareDependentRequired(old, newS, path)); + + // 13. dependentSchemas changes + changes.push(...compareDependentSchemas(old, newS, path, depth)); + + // 14. unevaluatedProperties changes + changes.push(...compareUnevaluatedProperties(old, newS, path, depth)); + + // 15. Array items changes (recurse) + changes.push(...compareArrayItems(old, newS, path, depth)); + + // 16. unevaluatedItems changes + changes.push(...compareUnevaluatedItems(old, newS, path, depth)); + + // 17. minContains/maxContains changes + changes.push(...compareMinMaxContains(old, newS, path)); + + // 18. Composition changes (anyOf, oneOf, allOf, if/then/else, not) + changes.push(...compareComposition(old, newS, path, depth)); + + return changes; +} + +/** + * Mapping of metadata keys to their change types + */ +const METADATA_CHANGE_TYPES: Readonly> = { + description: 'description-changed', + title: 'title-changed', + default: 'default-changed', + examples: 'examples-changed', +} as const; + +/** + * Compare metadata fields (description, title, default, examples) + */ +function compareMetadata( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + for (const key of METADATA_KEYS) { + const oldValue = oldSchema[key]; + const newValue = newSchema[key]; + + if (!deepEqual(oldValue, newValue)) { + changes.push({ + path: joinPath(path, key), + type: METADATA_CHANGE_TYPES[key], + oldValue, + newValue, + }); + } + } + + return changes; +} + +/** + * Mapping of annotation keys to their change types + */ +const ANNOTATION_CHANGE_TYPES: Readonly> = { + deprecated: 'deprecated-changed', + readOnly: 'read-only-changed', + writeOnly: 'write-only-changed', +} as const; + +/** + * Compare annotation fields (deprecated, readOnly, writeOnly) + * These are patch-level changes per Strands API + */ +function compareAnnotations( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + for (const key of ANNOTATION_KEYS) { + const oldValue = oldSchema[key]; + const newValue = newSchema[key]; + + if (oldValue !== newValue) { + changes.push({ + path: joinPath(path, key), + type: ANNOTATION_CHANGE_TYPES[key], + oldValue, + newValue, + }); + } + } + + return changes; +} + +/** + * Mapping of content keys to their change types + */ +const CONTENT_CHANGE_TYPES: Readonly> = { + contentEncoding: 'content-encoding-changed', + contentMediaType: 'content-media-type-changed', + contentSchema: 'content-schema-changed', +} as const; + +/** + * Compare content keywords (contentEncoding, contentMediaType, contentSchema) + * These are patch-level changes per Strands API + */ +function compareContentKeywords( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + // Compare contentEncoding and contentMediaType (simple string values) + for (const key of ['contentEncoding', 'contentMediaType'] as const) { + const oldValue = oldSchema[key]; + const newValue = newSchema[key]; + + if (oldValue !== newValue) { + changes.push({ + path: joinPath(path, key), + type: CONTENT_CHANGE_TYPES[key], + oldValue, + newValue, + }); + } + } + + // Compare contentSchema (recurse into schema) + const oldContentSchema = oldSchema.contentSchema; + const newContentSchema = newSchema.contentSchema; + + if (!deepEqual(oldContentSchema, newContentSchema)) { + if (isSchemaObject(oldContentSchema) && isSchemaObject(newContentSchema)) { + // Both are schemas - recurse but wrap all changes as content-schema-changed + const nestedChanges = walkInternal( + oldContentSchema, + newContentSchema, + joinPath(path, 'contentSchema'), + depth + 1 + ); + // If there are nested changes, report as content-schema-changed + if (nestedChanges.length > 0) { + changes.push({ + path: joinPath(path, 'contentSchema'), + type: 'content-schema-changed', + oldValue: oldContentSchema, + newValue: newContentSchema, + }); + } + } else { + // Schema added, removed, or type changed + changes.push({ + path: joinPath(path, 'contentSchema'), + type: 'content-schema-changed', + oldValue: oldContentSchema, + newValue: newContentSchema, + }); + } + } + + return changes; +} + +/** + * Compare type field, detecting narrowed/widened/changed + */ +function compareType( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + const oldType = normalizeType(oldSchema.type); + const newType = normalizeType(newSchema.type); + + // No type defined in either + if (oldType.length === 0 && newType.length === 0) { + return changes; + } + + // Types are identical + if (arraysEqual(oldType, newType)) { + return changes; + } + + // Determine change type + const changeType = determineTypeChange(oldType, newType); + + changes.push({ + path: joinPath(path, 'type'), + type: changeType, + oldValue: oldSchema.type, + newValue: newSchema.type, + }); + + return changes; +} + +/** + * Determine if type change is narrowed, widened, or changed + */ +function determineTypeChange(oldType: NormalizedType, newType: NormalizedType): ChangeType { + // Type added where none existed + if (oldType.length === 0 && newType.length > 0) { + return 'type-narrowed'; // Adding type constraint narrows + } + + // Type removed where one existed + if (oldType.length > 0 && newType.length === 0) { + return 'type-widened'; // Removing type constraint widens + } + + // Check if new is subset of old (narrowed) + const oldSet = new Set(oldType); + const newSet = new Set(newType); + + const isSubset = newType.every((t) => oldSet.has(t)); + const isSuperset = oldType.every((t) => newSet.has(t)); + + if (isSubset && !isSuperset) { + return 'type-narrowed'; // New allows fewer types + } + + if (isSuperset && !isSubset) { + return 'type-widened'; // New allows more types + } + + // Types changed incompatibly (neither subset nor superset) + return 'type-changed'; +} + +/** + * Compare enum values + */ +function compareEnum( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + const oldEnum = oldSchema.enum; + const newEnum = newSchema.enum; + + // Enum added + if (oldEnum === undefined && newEnum !== undefined) { + changes.push({ + path: joinPath(path, 'enum'), + type: 'enum-added', + oldValue: undefined, + newValue: newEnum, + }); + return changes; + } + + // Enum removed + if (oldEnum !== undefined && newEnum === undefined) { + changes.push({ + path: joinPath(path, 'enum'), + type: 'enum-removed', + oldValue: oldEnum, + newValue: undefined, + }); + return changes; + } + + // Both have enums - compare values + if (oldEnum !== undefined && newEnum !== undefined) { + // Find removed values + for (const oldValue of oldEnum) { + const exists = newEnum.some((v) => deepEqual(v, oldValue)); + if (!exists) { + changes.push({ + path: joinPath(path, 'enum'), + type: 'enum-value-removed', + oldValue, + newValue: undefined, + }); + } + } + + // Find added values + for (const newValue of newEnum) { + const exists = oldEnum.some((v) => deepEqual(v, newValue)); + if (!exists) { + changes.push({ + path: joinPath(path, 'enum'), + type: 'enum-value-added', + oldValue: undefined, + newValue, + }); + } + } + } + + // Also check const (single-value enum) + if (!deepEqual(oldSchema.const, newSchema.const)) { + if (oldSchema.const === undefined && newSchema.const !== undefined) { + changes.push({ + path: joinPath(path, 'const'), + type: 'enum-added', + oldValue: undefined, + newValue: newSchema.const, + }); + } else if (oldSchema.const !== undefined && newSchema.const === undefined) { + changes.push({ + path: joinPath(path, 'const'), + type: 'enum-removed', + oldValue: oldSchema.const, + newValue: undefined, + }); + } else { + changes.push({ + path: joinPath(path, 'const'), + type: 'enum-value-removed', + oldValue: oldSchema.const, + newValue: newSchema.const, + }); + } + } + + return changes; +} + +/** + * Compare format field + */ +function compareFormat( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + const oldFormat = oldSchema.format; + const newFormat = newSchema.format; + + if (oldFormat === newFormat) { + return changes; + } + + if (oldFormat === undefined && newFormat !== undefined) { + changes.push({ + path: joinPath(path, 'format'), + type: 'format-added', + oldValue: undefined, + newValue: newFormat, + }); + } else if (oldFormat !== undefined && newFormat === undefined) { + changes.push({ + path: joinPath(path, 'format'), + type: 'format-removed', + oldValue: oldFormat, + newValue: undefined, + }); + } else { + changes.push({ + path: joinPath(path, 'format'), + type: 'format-changed', + oldValue: oldFormat, + newValue: newFormat, + }); + } + + return changes; +} + +/** + * Compare numeric and string constraints + */ +function compareConstraints( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + for (const key of CONSTRAINT_KEYS) { + const oldValue = oldSchema[key]; + const newValue = newSchema[key]; + + if (deepEqual(oldValue, newValue)) { + continue; + } + + const direction = CONSTRAINT_DIRECTION[key]; + const changeType = determineConstraintChange(key, direction, oldValue, newValue); + + changes.push({ + path: joinPath(path, key), + type: changeType, + oldValue, + newValue, + }); + } + + return changes; +} + +/** + * Determine if constraint change is tightened or loosened + */ +function determineConstraintChange( + key: ConstraintKey, + direction: 'min' | 'max' | 'exact', + oldValue: unknown, + newValue: unknown +): ChangeType { + // Handle special cases for minItems/maxItems + if (key === 'minItems') { + if (oldValue === undefined && typeof newValue === 'number') { + return 'min-items-increased'; + } + if (typeof oldValue === 'number' && newValue === undefined) { + return 'constraint-loosened'; + } + if (typeof oldValue === 'number' && typeof newValue === 'number') { + return newValue > oldValue ? 'min-items-increased' : 'constraint-loosened'; + } + } + + if (key === 'maxItems') { + if (oldValue === undefined && typeof newValue === 'number') { + return 'max-items-decreased'; + } + if (typeof oldValue === 'number' && newValue === undefined) { + return 'constraint-loosened'; + } + if (typeof oldValue === 'number' && typeof newValue === 'number') { + return newValue < oldValue ? 'max-items-decreased' : 'constraint-loosened'; + } + } + + // Pattern and multipleOf are exact - any change is significant + if (direction === 'exact') { + return 'constraint-tightened'; // Conservative: treat as tightening + } + + // For min constraints: increasing tightens, decreasing loosens + if (direction === 'min') { + if (oldValue === undefined && newValue !== undefined) { + return 'constraint-tightened'; // Adding min constraint tightens + } + if (oldValue !== undefined && newValue === undefined) { + return 'constraint-loosened'; // Removing min constraint loosens + } + if (typeof oldValue === 'number' && typeof newValue === 'number') { + return newValue > oldValue ? 'constraint-tightened' : 'constraint-loosened'; + } + } + + // For max constraints: decreasing tightens, increasing loosens + if (direction === 'max') { + if (oldValue === undefined && newValue !== undefined) { + return 'constraint-tightened'; // Adding max constraint tightens + } + if (oldValue !== undefined && newValue === undefined) { + return 'constraint-loosened'; // Removing max constraint loosens + } + if (typeof oldValue === 'number' && typeof newValue === 'number') { + return newValue < oldValue ? 'constraint-tightened' : 'constraint-loosened'; + } + } + + // Handle uniqueItems specially + if (key === 'uniqueItems') { + if (newValue === true && oldValue !== true) { + return 'constraint-tightened'; + } + if (newValue !== true && oldValue === true) { + return 'constraint-loosened'; + } + } + + return 'constraint-tightened'; // Conservative default +} + +/** + * Compare object properties (recursive) + */ +function compareProperties( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldProps = oldSchema.properties ?? {}; + const newProps = newSchema.properties ?? {}; + + const oldKeys = new Set(Object.keys(oldProps)); + const newKeys = new Set(Object.keys(newProps)); + + // Find removed properties + for (const key of oldKeys) { + if (!newKeys.has(key)) { + changes.push({ + path: joinPath(path, 'properties', key), + type: 'property-removed', + oldValue: oldProps[key], + newValue: undefined, + }); + } + } + + // Find added properties + for (const key of newKeys) { + if (!oldKeys.has(key)) { + changes.push({ + path: joinPath(path, 'properties', key), + type: 'property-added', + oldValue: undefined, + newValue: newProps[key], + }); + } + } + + // Recurse into common properties + for (const key of oldKeys) { + if (newKeys.has(key)) { + const nestedChanges = walkInternal( + oldProps[key], + newProps[key], + joinPath(path, 'properties', key), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + // Also compare patternProperties if present + const oldPatternProps = oldSchema.patternProperties ?? {}; + const newPatternProps = newSchema.patternProperties ?? {}; + const oldPatternKeys = new Set(Object.keys(oldPatternProps)); + const newPatternKeys = new Set(Object.keys(newPatternProps)); + + for (const pattern of oldPatternKeys) { + if (!newPatternKeys.has(pattern)) { + changes.push({ + path: joinPath(path, 'patternProperties', pattern), + type: 'property-removed', + oldValue: oldPatternProps[pattern], + newValue: undefined, + }); + } + } + + for (const pattern of newPatternKeys) { + if (!oldPatternKeys.has(pattern)) { + changes.push({ + path: joinPath(path, 'patternProperties', pattern), + type: 'property-added', + oldValue: undefined, + newValue: newPatternProps[pattern], + }); + } + } + + for (const pattern of oldPatternKeys) { + if (newPatternKeys.has(pattern)) { + const nestedChanges = walkInternal( + oldPatternProps[pattern], + newPatternProps[pattern], + joinPath(path, 'patternProperties', pattern), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + return changes; +} + +/** + * Compare required array + */ +function compareRequired( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + const oldRequired = new Set(oldSchema.required ?? []); + const newRequired = new Set(newSchema.required ?? []); + + // Find removed required fields + for (const field of oldRequired) { + if (!newRequired.has(field)) { + changes.push({ + path: joinPath(path, 'required'), + type: 'required-removed', + oldValue: field, + newValue: undefined, + }); + } + } + + // Find added required fields + for (const field of newRequired) { + if (!oldRequired.has(field)) { + changes.push({ + path: joinPath(path, 'required'), + type: 'required-added', + oldValue: undefined, + newValue: field, + }); + } + } + + return changes; +} + +/** + * Compare additionalProperties + */ +function compareAdditionalProperties( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldAP = oldSchema.additionalProperties; + const newAP = newSchema.additionalProperties; + + // No change + if (deepEqual(oldAP, newAP)) { + return changes; + } + + // Normalize: undefined means allowed (true) + const oldAllows = oldAP !== false; + const newAllows = newAP !== false; + + // Check for denied/allowed transitions + if (oldAllows && !newAllows) { + changes.push({ + path: joinPath(path, 'additionalProperties'), + type: 'additional-properties-denied', + oldValue: oldAP, + newValue: newAP, + }); + return changes; + } + + if (!oldAllows && newAllows) { + changes.push({ + path: joinPath(path, 'additionalProperties'), + type: 'additional-properties-allowed', + oldValue: oldAP, + newValue: newAP, + }); + return changes; + } + + // Both allow but with different schemas + if (isSchemaObject(oldAP) && isSchemaObject(newAP)) { + const nestedChanges = walkInternal( + oldAP, + newAP, + joinPath(path, 'additionalProperties'), + depth + 1 + ); + if (nestedChanges.length > 0) { + changes.push(...nestedChanges); + } + return changes; + } + + // One is boolean, one is schema, or other differences + if (oldAP !== newAP) { + changes.push({ + path: joinPath(path, 'additionalProperties'), + type: 'additional-properties-changed', + oldValue: oldAP, + newValue: newAP, + }); + } + + return changes; +} + +/** + * Compare propertyNames schema + */ +function comparePropertyNames( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldPN = oldSchema.propertyNames; + const newPN = newSchema.propertyNames; + + if (deepEqual(oldPN, newPN)) { + return changes; + } + + // Any change to propertyNames is complex and requires manual review + if (isSchemaObject(oldPN) && isSchemaObject(newPN)) { + // Could recurse, but propertyNames changes are fundamentally unknown + changes.push({ + path: joinPath(path, 'propertyNames'), + type: 'property-names-changed', + oldValue: oldPN, + newValue: newPN, + }); + } else if (oldPN !== undefined || newPN !== undefined) { + changes.push({ + path: joinPath(path, 'propertyNames'), + type: 'property-names-changed', + oldValue: oldPN, + newValue: newPN, + }); + } + + return changes; +} + +/** + * Compare dependentRequired (Draft 2019-09+) + */ +function compareDependentRequired( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + const oldDR = oldSchema.dependentRequired ?? {}; + const newDR = newSchema.dependentRequired ?? {}; + + const oldKeys = new Set(Object.keys(oldDR)); + const newKeys = new Set(Object.keys(newDR)); + + // Find removed dependencies (non-breaking) + for (const key of oldKeys) { + if (!newKeys.has(key)) { + changes.push({ + path: joinPath(path, 'dependentRequired', key), + type: 'dependent-required-removed', + oldValue: oldDR[key], + newValue: undefined, + }); + } + } + + // Find added dependencies (breaking) + for (const key of newKeys) { + if (!oldKeys.has(key)) { + changes.push({ + path: joinPath(path, 'dependentRequired', key), + type: 'dependent-required-added', + oldValue: undefined, + newValue: newDR[key], + }); + } + } + + // Compare modified dependencies + for (const key of oldKeys) { + if (newKeys.has(key)) { + const oldReqs = new Set(oldDR[key] ?? []); + const newReqs = new Set(newDR[key] ?? []); + + // Find removed requirements (non-breaking) + for (const req of oldReqs) { + if (!newReqs.has(req)) { + changes.push({ + path: joinPath(path, 'dependentRequired', key), + type: 'dependent-required-removed', + oldValue: req, + newValue: undefined, + }); + } + } + + // Find added requirements (breaking) + for (const req of newReqs) { + if (!oldReqs.has(req)) { + changes.push({ + path: joinPath(path, 'dependentRequired', key), + type: 'dependent-required-added', + oldValue: undefined, + newValue: req, + }); + } + } + } + } + + return changes; +} + +/** + * Compare dependentSchemas (Draft 2019-09+) + */ +function compareDependentSchemas( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldDS = oldSchema.dependentSchemas ?? {}; + const newDS = newSchema.dependentSchemas ?? {}; + + const oldKeys = new Set(Object.keys(oldDS)); + const newKeys = new Set(Object.keys(newDS)); + const allKeys = new Set([...oldKeys, ...newKeys]); + + for (const key of allKeys) { + const oldValue = oldDS[key]; + const newValue = newDS[key]; + + if (!deepEqual(oldValue, newValue)) { + // dependentSchemas changes require manual review + changes.push({ + path: joinPath(path, 'dependentSchemas', key), + type: 'dependent-schemas-changed', + oldValue, + newValue, + }); + } + } + + return changes; +} + +/** + * Compare unevaluatedProperties (Draft 2019-09+) + */ +function compareUnevaluatedProperties( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldUP = oldSchema.unevaluatedProperties; + const newUP = newSchema.unevaluatedProperties; + + if (deepEqual(oldUP, newUP)) { + return changes; + } + + // unevaluatedProperties changes require manual review + changes.push({ + path: joinPath(path, 'unevaluatedProperties'), + type: 'unevaluated-properties-changed', + oldValue: oldUP, + newValue: newUP, + }); + + return changes; +} + +/** + * Compare array items schema (recursive) + */ +function compareArrayItems( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldItems = oldSchema.items; + const newItems = newSchema.items; + + // Compare single items schema + if (isSchemaObject(oldItems) && isSchemaObject(newItems)) { + const nestedChanges = walkInternal(oldItems, newItems, joinPath(path, 'items'), depth + 1); + changes.push(...nestedChanges); + } else if (oldItems !== undefined || newItems !== undefined) { + // Items added, removed, or changed type (array to object or vice versa) + if (!deepEqual(oldItems, newItems)) { + // Handle tuple items (array of schemas) + if (Array.isArray(oldItems) && Array.isArray(newItems)) { + const maxLen = Math.max(oldItems.length, newItems.length); + for (let i = 0; i < maxLen; i++) { + const nestedChanges = walkInternal( + oldItems[i], + newItems[i], + joinPath(path, 'items', String(i)), + depth + 1 + ); + changes.push(...nestedChanges); + } + } else if (Array.isArray(oldItems) !== Array.isArray(newItems)) { + // Structural change between tuple and list validation + changes.push({ + path: joinPath(path, 'items'), + type: 'items-changed', + oldValue: oldItems, + newValue: newItems, + }); + } else if (oldItems === undefined || newItems === undefined) { + changes.push({ + path: joinPath(path, 'items'), + type: 'items-changed', + oldValue: oldItems, + newValue: newItems, + }); + } + } + } + + // Compare prefixItems (JSON Schema draft 2020-12) + const oldPrefixItems = oldSchema.prefixItems; + const newPrefixItems = newSchema.prefixItems; + + if (Array.isArray(oldPrefixItems) || Array.isArray(newPrefixItems)) { + const oldArr = oldPrefixItems ?? []; + const newArr = newPrefixItems ?? []; + const maxLen = Math.max(oldArr.length, newArr.length); + + for (let i = 0; i < maxLen; i++) { + const nestedChanges = walkInternal( + oldArr[i], + newArr[i], + joinPath(path, 'prefixItems', String(i)), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + // Compare contains schema + if (oldSchema.contains !== undefined || newSchema.contains !== undefined) { + if (!deepEqual(oldSchema.contains, newSchema.contains)) { + if (isSchemaObject(oldSchema.contains) && isSchemaObject(newSchema.contains)) { + const nestedChanges = walkInternal( + oldSchema.contains, + newSchema.contains, + joinPath(path, 'contains'), + depth + 1 + ); + changes.push(...nestedChanges); + } else { + changes.push({ + path: joinPath(path, 'contains'), + type: 'items-changed', + oldValue: oldSchema.contains, + newValue: newSchema.contains, + }); + } + } + } + + return changes; +} + +/** + * Compare unevaluatedItems (Draft 2020-12) + */ +function compareUnevaluatedItems( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldUI = oldSchema.unevaluatedItems; + const newUI = newSchema.unevaluatedItems; + + if (deepEqual(oldUI, newUI)) { + return changes; + } + + // unevaluatedItems changes require manual review + changes.push({ + path: joinPath(path, 'unevaluatedItems'), + type: 'unevaluated-items-changed', + oldValue: oldUI, + newValue: newUI, + }); + + return changes; +} + +/** + * Compare minContains and maxContains (Draft 2019-09+) + */ +function compareMinMaxContains( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string +): RawChange[] { + const changes: RawChange[] = []; + + // Compare minContains + const oldMinContains = oldSchema.minContains; + const newMinContains = newSchema.minContains; + + if (oldMinContains !== newMinContains) { + changes.push({ + path: joinPath(path, 'minContains'), + type: 'min-contains-changed', + oldValue: oldMinContains, + newValue: newMinContains, + }); + } + + // Compare maxContains + const oldMaxContains = oldSchema.maxContains; + const newMaxContains = newSchema.maxContains; + + if (oldMaxContains !== newMaxContains) { + changes.push({ + path: joinPath(path, 'maxContains'), + type: 'max-contains-changed', + oldValue: oldMaxContains, + newValue: newMaxContains, + }); + } + + return changes; +} + +/** + * Compare composition keywords (anyOf, oneOf, allOf, if/then/else, not) + * + * Provides detailed analysis of composition changes with granular change types + * aligned with Strands API classification. + */ +function compareComposition( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + // Compare anyOf + changes.push(...compareAnyOf(oldSchema, newSchema, path, depth)); + + // Compare oneOf + changes.push(...compareOneOf(oldSchema, newSchema, path, depth)); + + // Compare allOf + changes.push(...compareAllOf(oldSchema, newSchema, path, depth)); + + // Compare not + changes.push(...compareNot(oldSchema, newSchema, path, depth)); + + // Compare if/then/else + changes.push(...compareIfThenElse(oldSchema, newSchema, path, depth)); + + return changes; +} + +/** + * Compare anyOf composition (option additions are breaking, removals are non-breaking) + */ +function compareAnyOf( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldAnyOf = oldSchema.anyOf; + const newAnyOf = newSchema.anyOf; + + // No anyOf in either + if (oldAnyOf === undefined && newAnyOf === undefined) { + return changes; + } + + // anyOf added + if (oldAnyOf === undefined && newAnyOf !== undefined) { + for (let i = 0; i < newAnyOf.length; i++) { + changes.push({ + path: joinPath(path, 'anyOf', String(i)), + type: 'anyof-option-added', + oldValue: undefined, + newValue: newAnyOf[i], + }); + } + return changes; + } + + // anyOf removed + if (oldAnyOf !== undefined && newAnyOf === undefined) { + for (let i = 0; i < oldAnyOf.length; i++) { + changes.push({ + path: joinPath(path, 'anyOf', String(i)), + type: 'anyof-option-removed', + oldValue: oldAnyOf[i], + newValue: undefined, + }); + } + return changes; + } + + // Both have anyOf - compare options + if (oldAnyOf !== undefined && newAnyOf !== undefined) { + const matched = matchCompositionOptions(oldAnyOf, newAnyOf); + + // Report removed options + for (const idx of matched.removed) { + changes.push({ + path: joinPath(path, 'anyOf', String(idx)), + type: 'anyof-option-removed', + oldValue: oldAnyOf[idx], + newValue: undefined, + }); + } + + // Report added options + for (const idx of matched.added) { + changes.push({ + path: joinPath(path, 'anyOf', String(idx)), + type: 'anyof-option-added', + oldValue: undefined, + newValue: newAnyOf[idx], + }); + } + + // Recurse into matched options for nested changes + for (const [oldIdx, newIdx] of matched.matched) { + const nestedChanges = walkInternal( + oldAnyOf[oldIdx], + newAnyOf[newIdx], + joinPath(path, 'anyOf', String(newIdx)), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + return changes; +} + +/** + * Compare oneOf composition (option additions are breaking, removals are non-breaking) + */ +function compareOneOf( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldOneOf = oldSchema.oneOf; + const newOneOf = newSchema.oneOf; + + // No oneOf in either + if (oldOneOf === undefined && newOneOf === undefined) { + return changes; + } + + // oneOf added + if (oldOneOf === undefined && newOneOf !== undefined) { + for (let i = 0; i < newOneOf.length; i++) { + changes.push({ + path: joinPath(path, 'oneOf', String(i)), + type: 'oneof-option-added', + oldValue: undefined, + newValue: newOneOf[i], + }); + } + return changes; + } + + // oneOf removed + if (oldOneOf !== undefined && newOneOf === undefined) { + for (let i = 0; i < oldOneOf.length; i++) { + changes.push({ + path: joinPath(path, 'oneOf', String(i)), + type: 'oneof-option-removed', + oldValue: oldOneOf[i], + newValue: undefined, + }); + } + return changes; + } + + // Both have oneOf - compare options + if (oldOneOf !== undefined && newOneOf !== undefined) { + const matched = matchCompositionOptions(oldOneOf, newOneOf); + + // Report removed options + for (const idx of matched.removed) { + changes.push({ + path: joinPath(path, 'oneOf', String(idx)), + type: 'oneof-option-removed', + oldValue: oldOneOf[idx], + newValue: undefined, + }); + } + + // Report added options + for (const idx of matched.added) { + changes.push({ + path: joinPath(path, 'oneOf', String(idx)), + type: 'oneof-option-added', + oldValue: undefined, + newValue: newOneOf[idx], + }); + } + + // Recurse into matched options for nested changes + for (const [oldIdx, newIdx] of matched.matched) { + const nestedChanges = walkInternal( + oldOneOf[oldIdx], + newOneOf[newIdx], + joinPath(path, 'oneOf', String(newIdx)), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + return changes; +} + +/** + * Compare allOf composition (member additions are breaking, removals are non-breaking) + */ +function compareAllOf( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldAllOf = oldSchema.allOf; + const newAllOf = newSchema.allOf; + + // No allOf in either + if (oldAllOf === undefined && newAllOf === undefined) { + return changes; + } + + // allOf added + if (oldAllOf === undefined && newAllOf !== undefined) { + for (let i = 0; i < newAllOf.length; i++) { + changes.push({ + path: joinPath(path, 'allOf', String(i)), + type: 'allof-member-added', + oldValue: undefined, + newValue: newAllOf[i], + }); + } + return changes; + } + + // allOf removed + if (oldAllOf !== undefined && newAllOf === undefined) { + for (let i = 0; i < oldAllOf.length; i++) { + changes.push({ + path: joinPath(path, 'allOf', String(i)), + type: 'allof-member-removed', + oldValue: oldAllOf[i], + newValue: undefined, + }); + } + return changes; + } + + // Both have allOf - compare members + if (oldAllOf !== undefined && newAllOf !== undefined) { + const matched = matchCompositionOptions(oldAllOf, newAllOf); + + // Report removed members + for (const idx of matched.removed) { + changes.push({ + path: joinPath(path, 'allOf', String(idx)), + type: 'allof-member-removed', + oldValue: oldAllOf[idx], + newValue: undefined, + }); + } + + // Report added members + for (const idx of matched.added) { + changes.push({ + path: joinPath(path, 'allOf', String(idx)), + type: 'allof-member-added', + oldValue: undefined, + newValue: newAllOf[idx], + }); + } + + // Recurse into matched members for nested changes + for (const [oldIdx, newIdx] of matched.matched) { + const nestedChanges = walkInternal( + oldAllOf[oldIdx], + newAllOf[newIdx], + joinPath(path, 'allOf', String(newIdx)), + depth + 1 + ); + changes.push(...nestedChanges); + } + } + + return changes; +} + +/** + * Compare not schema (any change is breaking) + */ +function compareNot( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldNot = oldSchema.not; + const newNot = newSchema.not; + + if (deepEqual(oldNot, newNot)) { + return changes; + } + + // Any change to not schema is breaking + changes.push({ + path: joinPath(path, 'not'), + type: 'not-schema-changed', + oldValue: oldNot, + newValue: newNot, + }); + + return changes; +} + +/** + * Compare if/then/else conditional schema (complex, requires manual review) + */ +function compareIfThenElse( + oldSchema: ResolvedSchema, + newSchema: ResolvedSchema, + path: string, + _depth: number +): RawChange[] { + const changes: RawChange[] = []; + + const oldIf = oldSchema.if; + const newIf = newSchema.if; + const oldThen = oldSchema.then; + const newThen = newSchema.then; + const oldElse = oldSchema.else; + const newElse = newSchema.else; + + // Check if any of the if/then/else keywords changed + const ifChanged = !deepEqual(oldIf, newIf); + const thenChanged = !deepEqual(oldThen, newThen); + const elseChanged = !deepEqual(oldElse, newElse); + + if (ifChanged || thenChanged || elseChanged) { + // Report as a single if-then-else-changed for simplicity + // These are complex and require manual review + changes.push({ + path: joinPath(path, 'if'), + type: 'if-then-else-changed', + oldValue: { if: oldIf, then: oldThen, else: oldElse }, + newValue: { if: newIf, then: newThen, else: newElse }, + }); + } + + return changes; +} + +/** + * Match composition options between old and new arrays + * Uses structural similarity to find corresponding options + */ +function matchCompositionOptions( + oldOptions: ResolvedSchema[], + newOptions: ResolvedSchema[] +): { + matched: Array<[number, number]>; + removed: number[]; + added: number[]; +} { + const matched: Array<[number, number]> = []; + const usedOld = new Set(); + const usedNew = new Set(); + + // First pass: find exact matches + for (let i = 0; i < oldOptions.length; i++) { + for (let j = 0; j < newOptions.length; j++) { + if (usedNew.has(j)) continue; + if (deepEqual(oldOptions[i], newOptions[j])) { + matched.push([i, j]); + usedOld.add(i); + usedNew.add(j); + break; + } + } + } + + // Second pass: match by position for remaining unmatched + // This handles cases where schemas are modified but at same position + for (let i = 0; i < oldOptions.length; i++) { + if (usedOld.has(i)) continue; + if (i < newOptions.length && !usedNew.has(i)) { + // Match by position as a heuristic + matched.push([i, i]); + usedOld.add(i); + usedNew.add(i); + } + } + + // Collect removed (in old but not matched) + const removed: number[] = []; + for (let i = 0; i < oldOptions.length; i++) { + if (!usedOld.has(i)) { + removed.push(i); + } + } + + // Collect added (in new but not matched) + const added: number[] = []; + for (let j = 0; j < newOptions.length; j++) { + if (!usedNew.has(j)) { + added.push(j); + } + } + + return { matched, removed, added }; +} diff --git a/packages/differs.core/tsconfig.build.json b/packages/differs.core/tsconfig.build.json new file mode 100644 index 0000000..2333a75 --- /dev/null +++ b/packages/differs.core/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "exclude": ["node_modules", "dist", "tests", "**/*.test.ts"] +} diff --git a/packages/differs.core/tsconfig.json b/packages/differs.core/tsconfig.json new file mode 100644 index 0000000..e666b16 --- /dev/null +++ b/packages/differs.core/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/contract/.eslintrc b/packages/differs.json-schema/.eslintrc similarity index 94% rename from packages/contract/.eslintrc rename to packages/differs.json-schema/.eslintrc index f1ca71a..5e998f8 100644 --- a/packages/contract/.eslintrc +++ b/packages/differs.json-schema/.eslintrc @@ -1,3 +1,3 @@ { "extends": "../../.eslintrc" -} \ No newline at end of file +} diff --git a/packages/differs.json-schema/.gitignore b/packages/differs.json-schema/.gitignore new file mode 100644 index 0000000..83631f8 --- /dev/null +++ b/packages/differs.json-schema/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.tsbuildinfo diff --git a/packages/differs.json-schema/CHANGELOG.md b/packages/differs.json-schema/CHANGELOG.md new file mode 100644 index 0000000..b3e6767 --- /dev/null +++ b/packages/differs.json-schema/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/differs.json-schema@0.1.0-dev.7...@contractual/differs.json-schema@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* bump rimraf from 5.0.10 to 6.1.3 ([#23](https://github.com/codotech/contractual/issues/23)) ([d8930e9](https://github.com/codotech/contractual/commit/d8930e9bdc69591a5125dc628ec6bbfc94ff4d2b)) +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/differs.json-schema/README.md b/packages/differs.json-schema/README.md new file mode 100644 index 0000000..98df287 --- /dev/null +++ b/packages/differs.json-schema/README.md @@ -0,0 +1,180 @@ +# @contractual/differs.json-schema + +Production-quality breaking change detection for JSON Schema. + +Detects and classifies structural changes between JSON Schema versions, recommending semantic version bumps (major/minor/patch). + +## Why? + +JSON Schema has 150M+ weekly npm downloads but **no production-quality breaking change detection tool**. This package fills that gap. + +## Installation + +```bash +npm install @contractual/differs.json-schema +``` + +## Usage + +### Compare Schema Files + +```typescript +import { diffJsonSchema } from '@contractual/differs.json-schema'; + +const result = await diffJsonSchema('v1/user.schema.json', 'v2/user.schema.json'); + +console.log(`Suggested bump: ${result.suggestedBump}`); +console.log(`Breaking: ${result.summary.breaking}`); +console.log(`Non-breaking: ${result.summary.nonBreaking}`); + +// List breaking changes +for (const change of result.changes) { + if (change.severity === 'breaking') { + console.log(`❌ ${change.message}`); + } +} +``` + +### Compare Schema Objects + +```typescript +import { diffJsonSchemaObjects } from '@contractual/differs.json-schema'; + +const oldSchema = { type: 'object', properties: { name: { type: 'string' } } }; +const newSchema = { type: 'object', properties: { name: { type: 'number' } } }; + +const result = diffJsonSchemaObjects(oldSchema, newSchema); +// result.suggestedBump === 'major' +// result.changes[0].message === 'Type changed from "string" to "number"...' +``` + +### Quick Breaking Check + +```typescript +import { hasBreakingChanges } from '@contractual/differs.json-schema'; + +if (hasBreakingChanges(oldSchema, newSchema)) { + throw new Error('Breaking changes detected!'); +} +``` + +## Change Classifications + +### Breaking Changes (β†’ major bump) + +| Category | Example | +|----------|---------| +| `property-removed` | Field deleted from schema | +| `required-added` | Existing field became required | +| `type-changed` | Type changed incompatibly | +| `type-narrowed` | Union type reduced | +| `enum-value-removed` | Enum option removed | +| `constraint-tightened` | minLength/maxLength made stricter | +| `additional-properties-denied` | Open schema closed | +| `min-items-increased` | Array minimum increased | +| `max-items-decreased` | Array maximum decreased | + +### Non-Breaking Changes (β†’ minor bump) + +| Category | Example | +|----------|---------| +| `property-added` | New optional field | +| `required-removed` | Field became optional | +| `type-widened` | Type accepts more values | +| `enum-value-added` | New enum option | +| `constraint-loosened` | Constraints relaxed | +| `additional-properties-allowed` | Closed schema opened | + +### Patch Changes (β†’ patch bump) + +| Category | Example | +|----------|---------| +| `description-changed` | Documentation updated | +| `title-changed` | Title updated | +| `default-changed` | Default value updated | +| `examples-changed` | Examples updated | + +## Advanced Usage + +### Resolve $refs + +```typescript +import { resolveRefs } from '@contractual/differs.json-schema'; + +const schema = { + type: 'object', + properties: { + user: { $ref: '#/$defs/User' } + }, + $defs: { + User: { type: 'object', properties: { name: { type: 'string' } } } + } +}; + +const { schema: resolved, warnings } = resolveRefs(schema); +// resolved.properties.user === { type: 'object', properties: { name: { type: 'string' } } } +``` + +### Custom Classification + +```typescript +import { walk, classify, CLASSIFICATION_SETS } from '@contractual/differs.json-schema'; + +// Get raw changes +const rawChanges = walk(resolvedOld, resolvedNew, ''); + +// Classify with custom rules +for (const change of rawChanges) { + if (CLASSIFICATION_SETS.BREAKING.has(change.type)) { + console.log('Breaking:', change.path); + } +} +``` + +## Supported JSON Schema Versions + +- Draft-07 (primary target) +- Draft 2019-09 +- Draft 2020-12 + +## API Reference + +### Main Functions + +- `diffJsonSchema(oldPath, newPath)` - Compare two schema files +- `diffJsonSchemaObjects(oldSchema, newSchema)` - Compare two schema objects +- `hasBreakingChanges(oldSchema, newSchema)` - Quick boolean check + +### Utilities + +- `resolveRefs(schema)` - Resolve all $ref pointers +- `walk(oldSchema, newSchema, basePath)` - Low-level schema walker +- `classify(change)` - Classify a raw change +- `classifyPropertyAdded(change, newSchema)` - Context-aware property classification + +### Types + +```typescript +interface DiffResult { + changes: Change[]; + summary: DiffSummary; + suggestedBump: 'major' | 'minor' | 'patch' | 'none'; +} + +interface Change { + path: string; + severity: 'breaking' | 'non-breaking' | 'patch' | 'unknown'; + category: string; + message: string; + oldValue?: unknown; + newValue?: unknown; +} +``` + +## Part of Contractual + +This package is extracted from [Contractual](https://github.com/contractual-dev/contractual), the schema contract lifecycle orchestrator. + +## License + +MIT diff --git a/packages/differs.json-schema/package.json b/packages/differs.json-schema/package.json new file mode 100644 index 0000000..ef29a3c --- /dev/null +++ b/packages/differs.json-schema/package.json @@ -0,0 +1,62 @@ +{ + "name": "@contractual/differs.json-schema", + "version": "0.1.0-beta.0", + "description": "Detect breaking changes between JSON Schema versions with semantic classification", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/differs.json-schema" + }, + "homepage": "https://github.com/codotech/contractual/tree/next/packages/differs.json-schema", + "bugs": { + "url": "https://github.com/codotech/contractual/issues" + }, + "keywords": [ + "json-schema", + "diff", + "breaking-changes", + "semver", + "schema", + "api", + "compatibility", + "migration", + "validation" + ], + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch", + "test": "vitest run --config ../../vitest.config.e2e.ts --root ../.. tests/e2e/09-differs.json-schema.test.ts", + "test:watch": "vitest", + "lint": "eslint \"src/**/*.ts\"" + }, + "files": [ + "dist", + "README.md" + ], + "dependencies": { + "@contractual/differs.core": "workspace:*", + "@contractual/types": "workspace:*" + }, + "devDependencies": { + "rimraf": "^6.1.3", + "vitest": "^4.1.11" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/differs.json-schema/src/compare.ts b/packages/differs.json-schema/src/compare.ts new file mode 100644 index 0000000..07f126b --- /dev/null +++ b/packages/differs.json-schema/src/compare.ts @@ -0,0 +1,293 @@ +/** + * Strands-compatible JSON Schema comparison API + * + * Provides schema comparison with output format matching the Strands API + * (https://strands.octue.com/api/compare-schemas) + */ + +import type { ChangeSeverity, RawChange } from '@contractual/types'; +import type { + CompareResult, + CompareOptions, + StrandsTrace, + StrandsCompatibility, + StrandsVersion, + SemanticVersion, +} from './types.js'; +import { resolveRefs, walk, classify, classifyPropertyAdded } from '@contractual/differs.core'; + +/** + * Compare two JSON Schema objects and return Strands-compatible result + * + * @param sourceSchema - The source (old/baseline) schema object + * @param targetSchema - The target (new/current) schema object + * @param options - Optional comparison options + * @returns CompareResult in Strands API format + * + * @example + * ```typescript + * const source = { type: 'object', properties: { name: { type: 'string' } } }; + * const target = { type: 'object', properties: { name: { type: 'number' } } }; + * + * const result = compareSchemas(source, target, { currentVersion: '1.0.0' }); + * console.log(result.version); // 'major' + * console.log(result.newVersion); // { major: 2, minor: 0, patch: 0, version: '2.0.0' } + * ``` + */ +export function compareSchemas( + sourceSchema: unknown, + targetSchema: unknown, + options?: CompareOptions +): CompareResult { + // Resolve $refs in both schemas + const resolvedSourceResult = resolveRefs(sourceSchema); + const resolvedTargetResult = resolveRefs(targetSchema); + + const resolvedSource = resolvedSourceResult.schema; + const resolvedTarget = resolvedTargetResult.schema; + + // Walk both schemas and detect raw changes + const rawChanges = walk(resolvedSource, resolvedTarget, ''); + + // Classify changes and generate traces + const traces: StrandsTrace[] = []; + let hasBreaking = false; + let hasNonBreaking = false; + let hasPatch = false; + let hasUnknown = false; + + for (const raw of rawChanges) { + const severity = classifyChange(raw, resolvedTarget); + + // Track highest severity + switch (severity) { + case 'breaking': + hasBreaking = true; + break; + case 'non-breaking': + hasNonBreaking = true; + break; + case 'patch': + hasPatch = true; + break; + case 'unknown': + hasUnknown = true; + break; + } + + // Generate trace + const trace = rawChangeToTrace(raw, severity); + traces.push(trace); + } + + // Determine version bump + const version = determineVersion(hasBreaking, hasNonBreaking, hasPatch, hasUnknown, traces); + + // Compute new version if current version provided + const newVersion = options?.currentVersion + ? computeNewVersion(options.currentVersion, version) + : null; + + return { + version, + traces, + newVersion, + }; +} + +/** + * Classify a change, with context-aware classification for property-added + */ +function classifyChange(change: RawChange, newSchema: unknown): ChangeSeverity { + if (change.type === 'property-added') { + return classifyPropertyAdded(change, newSchema); + } + return classify(change); +} + +/** + * Convert a RawChange to a Strands trace + */ +function rawChangeToTrace(change: RawChange, severity: ChangeSeverity): StrandsTrace { + const compatibility = severityToCompatibility(severity); + + // Determine left/right paths based on change type + const isAddition = + change.type.includes('added') || + change.type === 'type-widened' || + change.type === 'constraint-loosened'; + const isRemoval = + change.type.includes('removed') || + change.type === 'type-narrowed' || + change.type === 'constraint-tightened'; + + let left: string | null = change.path; + let right: string | null = change.path; + + // For additions, left is null (didn't exist in source) + if (isAddition && change.oldValue === undefined) { + left = null; + } + + // For removals, right is null (doesn't exist in target) + if (isRemoval && change.newValue === undefined) { + right = null; + } + + return { + compatibility, + left, + right, + }; +} + +/** + * Map ChangeSeverity to Strands compatibility + */ +function severityToCompatibility(severity: ChangeSeverity): StrandsCompatibility { + switch (severity) { + case 'breaking': + return 'incompatible'; + case 'non-breaking': + case 'patch': + return 'compatible'; + case 'unknown': + default: + return 'unknown'; + } +} + +/** + * Determine the version bump level based on detected changes + */ +function determineVersion( + hasBreaking: boolean, + hasNonBreaking: boolean, + hasPatch: boolean, + hasUnknown: boolean, + traces: StrandsTrace[] +): StrandsVersion { + // No changes = equal + if (traces.length === 0) { + return 'equal'; + } + + // If there are unknown changes, we can't determine version + if (hasUnknown && !hasBreaking && !hasNonBreaking) { + return null; + } + + // Priority: breaking > non-breaking > patch + if (hasBreaking) { + return 'major'; + } + + if (hasNonBreaking) { + return 'minor'; + } + + if (hasPatch) { + return 'patch'; + } + + // Only unknown changes + return null; +} + +/** + * Compute the new semantic version based on current version and bump level + */ +function computeNewVersion( + currentVersion: string, + bumpLevel: StrandsVersion +): SemanticVersion | null { + if (bumpLevel === null || bumpLevel === 'equal') { + // Parse current version and return as-is for equal + const parsed = parseVersion(currentVersion); + if (!parsed) return null; + return bumpLevel === 'equal' ? parsed : null; + } + + const parsed = parseVersion(currentVersion); + if (!parsed) { + return null; + } + + let { major, minor, patch } = parsed; + + switch (bumpLevel) { + case 'major': + major += 1; + minor = 0; + patch = 0; + break; + case 'minor': + minor += 1; + patch = 0; + break; + case 'patch': + patch += 1; + break; + } + + return { + major, + minor, + patch, + version: `${major}.${minor}.${patch}`, + }; +} + +/** + * Parse a semver string into components + */ +function parseVersion(version: string): SemanticVersion | null { + // Remove 'v' prefix if present + const cleaned = version.startsWith('v') ? version.slice(1) : version; + + // Match semver pattern (with optional prerelease/build) + const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) { + return null; + } + + const major = parseInt(match[1]!, 10); + const minor = parseInt(match[2]!, 10); + const patch = parseInt(match[3]!, 10); + + return { + major, + minor, + patch, + version: `${major}.${minor}.${patch}`, + }; +} + +/** + * Compare two JSON Schema objects and return a simple compatibility result + * + * Convenience function for quick compatibility checks. + * + * @param sourceSchema - The source (old/baseline) schema object + * @param targetSchema - The target (new/current) schema object + * @returns 'compatible' | 'incompatible' | 'unknown' + */ +export function checkCompatibility( + sourceSchema: unknown, + targetSchema: unknown +): StrandsCompatibility { + const result = compareSchemas(sourceSchema, targetSchema); + + // If any trace is incompatible, overall is incompatible + if (result.traces.some((t) => t.compatibility === 'incompatible')) { + return 'incompatible'; + } + + // If any trace is unknown, overall is unknown + if (result.traces.some((t) => t.compatibility === 'unknown')) { + return 'unknown'; + } + + // All compatible + return 'compatible'; +} diff --git a/packages/differs.json-schema/src/differ.ts b/packages/differs.json-schema/src/differ.ts new file mode 100644 index 0000000..c533b4a --- /dev/null +++ b/packages/differs.json-schema/src/differ.ts @@ -0,0 +1,78 @@ +/** + * JSON Schema Structural Differ + * + * Compares two JSON Schema files and detects structural changes, + * classifying them by severity for semver bump decisions. + */ + +import { readFile } from 'node:fs/promises'; +import type { DiffResult } from '@contractual/types'; +import { resolveRefs, walk, assembleResult, formatChangeMessage } from '@contractual/differs.core'; + +// Re-export for backward compatibility +export { formatChangeMessage }; + +/** + * Diff two JSON Schema files and detect structural changes + * + * @param oldPath - Path to the old/base schema file + * @param newPath - Path to the new/changed schema file + * @returns DiffResult with classified changes and suggested bump + */ +export async function diffJsonSchema(oldPath: string, newPath: string): Promise { + let oldContent: string; + let newContent: string; + + try { + oldContent = await readFile(oldPath, 'utf-8'); + } catch (error) { + throw new Error( + `Failed to read old schema file "${oldPath}": ${error instanceof Error ? error.message : String(error)}` + ); + } + + try { + newContent = await readFile(newPath, 'utf-8'); + } catch (error) { + throw new Error( + `Failed to read new schema file "${newPath}": ${error instanceof Error ? error.message : String(error)}` + ); + } + + let oldSchema: unknown; + let newSchema: unknown; + + try { + oldSchema = JSON.parse(oldContent); + } catch (error) { + throw new Error( + `Failed to parse old schema as JSON "${oldPath}": ${error instanceof Error ? error.message : String(error)}` + ); + } + + try { + newSchema = JSON.parse(newContent); + } catch (error) { + throw new Error( + `Failed to parse new schema as JSON "${newPath}": ${error instanceof Error ? error.message : String(error)}` + ); + } + + return diffJsonSchemaObjects(oldSchema, newSchema); +} + +/** + * Diff two JSON Schema objects and detect structural changes + * + * @param oldSchema - The old/base schema object + * @param newSchema - The new/changed schema object + * @returns DiffResult with classified changes and suggested bump + */ +export function diffJsonSchemaObjects(oldSchema: unknown, newSchema: unknown): DiffResult { + const resolvedOld = resolveRefs(oldSchema).schema; + const resolvedNew = resolveRefs(newSchema).schema; + + const rawChanges = walk(resolvedOld, resolvedNew, ''); + + return assembleResult(rawChanges, resolvedNew); +} diff --git a/packages/differs.json-schema/src/index.ts b/packages/differs.json-schema/src/index.ts new file mode 100644 index 0000000..a23c624 --- /dev/null +++ b/packages/differs.json-schema/src/index.ts @@ -0,0 +1,81 @@ +/** + * @contractual/differs.json-schema + * + * Detect and classify breaking changes between JSON Schema versions. + * + * @packageDocumentation + */ + +// ============================================================================= +// Strands-compatible API (primary) +// ============================================================================= + +export { compareSchemas, checkCompatibility } from './compare.js'; + +// ============================================================================= +// Legacy file-based API (backward compatible) +// ============================================================================= + +export { diffJsonSchema, diffJsonSchemaObjects, formatChangeMessage } from './differ.js'; + +// ============================================================================= +// Re-exports from @contractual/differs.core (backward compatibility) +// ============================================================================= + +export { + // Classification + classify, + classifyPropertyAdded, + classifyAll, + CLASSIFICATION_SETS, + // Ref resolution + resolveRefs, + hasUnresolvedRefs, + extractRefs, + validateRefs, + type ResolveResult, + // Walker + walk, + // Result assembly + assembleResult, + type AssembleOptions, + // Schema types and utilities + type ResolvedSchema, + type JSONSchemaType, + type NormalizedType, + type ConstraintKey, + type ConstraintDirection, + type CompositionKeyword, + type MetadataKey, + type AnnotationKey, + type ContentKey, + type WalkerContext, + isSchemaObject, + isSchemaArray, + normalizeType, + arraysEqual, + deepEqual, + escapeJsonPointer, + joinPath, + CONSTRAINT_KEYS, + CONSTRAINT_DIRECTION, + COMPOSITION_KEYWORDS, + METADATA_KEYS, + ANNOTATION_KEYS, + CONTENT_KEYS, + DEFAULT_MAX_DEPTH, +} from '@contractual/differs.core'; + +// ============================================================================= +// Strands API Types +// ============================================================================= + +export type { + CompareResult, + CompareOptions, + StrandsTrace, + StrandsCompatibility, + StrandsVersion, + SemanticVersion, + JsonSchemaDraft, +} from './types.js'; diff --git a/packages/differs.json-schema/src/types.ts b/packages/differs.json-schema/src/types.ts new file mode 100644 index 0000000..4ca6e5a --- /dev/null +++ b/packages/differs.json-schema/src/types.ts @@ -0,0 +1,67 @@ +/** + * Strands API types for JSON Schema comparison + * + * These types are specific to the Strands-compatible comparison API. + * Core schema diffing types are in @contractual/differs.core. + */ + +/** + * Strands compatibility classification for a trace + */ +export type StrandsCompatibility = 'incompatible' | 'compatible' | 'unknown'; + +/** + * Strands version bump level + */ +export type StrandsVersion = 'equal' | 'patch' | 'minor' | 'major' | null; + +/** + * A single trace showing where schemas differ (Strands format) + */ +export interface StrandsTrace { + /** Compatibility status of this change */ + readonly compatibility: StrandsCompatibility; + /** JSON Pointer path in the source (left) schema, null if added */ + readonly left: string | null; + /** JSON Pointer path in the target (right) schema, null if removed */ + readonly right: string | null; +} + +/** + * Semantic version object + */ +export interface SemanticVersion { + readonly major: number; + readonly minor: number; + readonly patch: number; + readonly version: string; +} + +/** + * Strands API response format + */ +export interface CompareResult { + /** Version bump level, null if unknown/error */ + readonly version: StrandsVersion; + /** List of traces showing where schemas differ */ + readonly traces: StrandsTrace[]; + /** Computed new version based on current version + bump */ + readonly newVersion: SemanticVersion | null; + /** Error or warning message */ + readonly message?: string; +} + +/** + * Options for schema comparison + */ +export interface CompareOptions { + /** Current version to compute newVersion from (e.g., "1.0.0") */ + readonly currentVersion?: string; + /** Draft version for validation rules */ + readonly draft?: JsonSchemaDraft; +} + +/** + * Supported JSON Schema drafts + */ +export type JsonSchemaDraft = 'draft-07' | 'draft-2019-09' | 'draft-2020-12'; diff --git a/packages/differs.json-schema/tsconfig.build.json b/packages/differs.json-schema/tsconfig.build.json new file mode 100644 index 0000000..2333a75 --- /dev/null +++ b/packages/differs.json-schema/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "exclude": ["node_modules", "dist", "tests", "**/*.test.ts"] +} diff --git a/packages/differs.json-schema/tsconfig.json b/packages/differs.json-schema/tsconfig.json new file mode 100644 index 0000000..e666b16 --- /dev/null +++ b/packages/differs.json-schema/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/differs.openapi/CHANGELOG.md b/packages/differs.openapi/CHANGELOG.md new file mode 100644 index 0000000..135afe9 --- /dev/null +++ b/packages/differs.openapi/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/differs.openapi@0.1.0-dev.7...@contractual/differs.openapi@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* bump rimraf from 5.0.10 to 6.1.3 ([#23](https://github.com/codotech/contractual/issues/23)) ([d8930e9](https://github.com/codotech/contractual/commit/d8930e9bdc69591a5125dc628ec6bbfc94ff4d2b)) +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/differs.openapi/package.json b/packages/differs.openapi/package.json new file mode 100644 index 0000000..2c10cb9 --- /dev/null +++ b/packages/differs.openapi/package.json @@ -0,0 +1,44 @@ +{ + "name": "@contractual/differs.openapi", + "private": false, + "version": "0.1.0-beta.0", + "description": "OpenAPI 3.0/3.1 breaking change detection for Contractual", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/differs.openapi" + }, + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc -p tsconfig.build.json" + }, + "files": [ + "dist", + "README.md" + ], + "dependencies": { + "@contractual/differs.core": "workspace:*", + "@contractual/types": "workspace:*", + "@redocly/openapi-core": "^1.34.20" + }, + "devDependencies": { + "rimraf": "^6.1.3" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/differs.openapi/src/differ.ts b/packages/differs.openapi/src/differ.ts new file mode 100644 index 0000000..2d0cc51 --- /dev/null +++ b/packages/differs.openapi/src/differ.ts @@ -0,0 +1,40 @@ +/** + * OpenAPI Differ + * + * Compares two OpenAPI specifications and detects breaking changes. + * Supports OpenAPI 3.0 and 3.1. + */ + +import type { DiffResult } from '@contractual/types'; +import { assembleResult } from '@contractual/differs.core'; +import { resolveOpenApiSpec } from './resolver.js'; +import { diffStructural } from './structural.js'; + +/** + * Diff two OpenAPI specification files + * + * @param oldSpecPath - Path to the old/base OpenAPI spec file + * @param newSpecPath - Path to the new/changed OpenAPI spec file + * @returns DiffResult with classified changes and suggested bump + */ +export async function diffOpenApi(oldSpecPath: string, newSpecPath: string): Promise { + const oldSpec = await resolveOpenApiSpec(oldSpecPath); + const newSpec = await resolveOpenApiSpec(newSpecPath); + + return diffOpenApiObjects(oldSpec, newSpec); +} + +/** + * Diff two resolved OpenAPI spec objects directly + * + * @param oldSpec - The old/base OpenAPI spec object (fully resolved) + * @param newSpec - The new/changed OpenAPI spec object (fully resolved) + * @returns DiffResult with classified changes and suggested bump + */ +export function diffOpenApiObjects( + oldSpec: Record, + newSpec: Record +): DiffResult { + const rawChanges = diffStructural(oldSpec, newSpec); + return assembleResult(rawChanges, newSpec); +} diff --git a/packages/differs.openapi/src/index.ts b/packages/differs.openapi/src/index.ts new file mode 100644 index 0000000..8ae0302 --- /dev/null +++ b/packages/differs.openapi/src/index.ts @@ -0,0 +1,3 @@ +export { diffOpenApi, diffOpenApiObjects } from './differ.js'; +export { resolveOpenApiSpec } from './resolver.js'; +export { diffStructural } from './structural.js'; diff --git a/packages/differs.openapi/src/resolver.ts b/packages/differs.openapi/src/resolver.ts new file mode 100644 index 0000000..4978e7a --- /dev/null +++ b/packages/differs.openapi/src/resolver.ts @@ -0,0 +1,42 @@ +/** + * OpenAPI spec resolver using @redocly/openapi-core + * + * Handles both OpenAPI 3.0 and 3.1 specifications. + * Uses Redocly to bundle external refs into a single document, + * then uses the core ref-resolver to dereference internal $refs + * (which handles circular references safely). + */ + +import { resolveRefs } from '@contractual/differs.core'; + +/** + * Resolve an OpenAPI spec file to a fully dereferenced object + * + * @param specPath - Absolute or relative path to the OpenAPI spec file + * @returns The fully resolved OpenAPI document as a plain object + */ +export async function resolveOpenApiSpec(specPath: string): Promise> { + let bundle: typeof import('@redocly/openapi-core').bundle; + let loadConfig: typeof import('@redocly/openapi-core').loadConfig; + + try { + const redocly = await import('@redocly/openapi-core'); + bundle = redocly.bundle; + loadConfig = redocly.loadConfig; + } catch { + throw new Error( + 'OpenAPI diffing requires @redocly/openapi-core. ' + + 'Install it with: npm install @redocly/openapi-core' + ); + } + + // Bundle only (no dereference) β€” resolves external refs into one document + // but keeps internal $refs intact to avoid circular object references + const config = await loadConfig(); + const result = await bundle({ ref: specPath, config }); + const bundled = result.bundle.parsed as Record; + + // Use our own ref-resolver which handles circular refs safely + const resolved = resolveRefs(bundled); + return resolved.schema as Record; +} diff --git a/packages/differs.openapi/src/structural.ts b/packages/differs.openapi/src/structural.ts new file mode 100644 index 0000000..49ce505 --- /dev/null +++ b/packages/differs.openapi/src/structural.ts @@ -0,0 +1,317 @@ +/** + * OpenAPI structural differ + * + * Compares the structural layer of two OpenAPI specs: + * paths, operations, parameters, request bodies, responses. + * Schema-level diffing is delegated to @contractual/differs.core walker. + */ + +import type { RawChange, ChangeType } from '@contractual/types'; +import { walk, escapeJsonPointer } from '@contractual/differs.core'; + +const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const; + +type PathsMap = Record>; +type OperationMap = Record; + +/** + * Diff the structural layer of two resolved OpenAPI specs + */ +export function diffStructural( + oldSpec: Record, + newSpec: Record +): RawChange[] { + const changes: RawChange[] = []; + + // Diff top-level info metadata + const oldInfo = (oldSpec.info ?? {}) as Record; + const newInfo = (newSpec.info ?? {}) as Record; + + if (oldInfo.description !== newInfo.description) { + changes.push({ + path: '/info/description', + type: 'description-changed' as ChangeType, + oldValue: oldInfo.description, + newValue: newInfo.description, + }); + } + + if (oldInfo.title !== newInfo.title) { + changes.push({ + path: '/info/title', + type: 'title-changed' as ChangeType, + oldValue: oldInfo.title, + newValue: newInfo.title, + }); + } + + const oldPaths = (oldSpec.paths ?? {}) as PathsMap; + const newPaths = (newSpec.paths ?? {}) as PathsMap; + + // Diff paths + for (const path of Object.keys(oldPaths)) { + const escapedPath = escapeJsonPointer(path); + + if (!(path in newPaths)) { + changes.push({ + path: `/paths/${escapedPath}`, + type: 'path-removed' as ChangeType, + oldValue: path, + }); + continue; + } + + // Diff operations within matching paths + changes.push(...diffOperations(oldPaths[path], newPaths[path], path)); + } + + for (const path of Object.keys(newPaths)) { + if (!(path in oldPaths)) { + const escapedPath = escapeJsonPointer(path); + changes.push({ + path: `/paths/${escapedPath}`, + type: 'path-added' as ChangeType, + newValue: path, + }); + } + } + + return changes; +} + +/** + * Diff operations (HTTP methods) within a path + */ +function diffOperations( + oldPathItem: Record, + newPathItem: Record, + apiPath: string +): RawChange[] { + const changes: RawChange[] = []; + const escapedPath = escapeJsonPointer(apiPath); + + for (const method of HTTP_METHODS) { + const oldOp = oldPathItem[method] as OperationMap | undefined; + const newOp = newPathItem[method] as OperationMap | undefined; + + if (oldOp && !newOp) { + changes.push({ + path: `/paths/${escapedPath}/${method}`, + type: 'operation-removed' as ChangeType, + oldValue: `${method.toUpperCase()} ${apiPath}`, + }); + } else if (!oldOp && newOp) { + changes.push({ + path: `/paths/${escapedPath}/${method}`, + type: 'operation-added' as ChangeType, + newValue: `${method.toUpperCase()} ${apiPath}`, + }); + } else if (oldOp && newOp) { + // Both exist β€” diff metadata, parameters, request body, responses + const basePath = `/paths/${escapedPath}/${method}`; + changes.push(...diffOperationMetadata(oldOp, newOp, basePath)); + changes.push(...diffParameters(oldOp, newOp, basePath)); + changes.push(...diffRequestBody(oldOp, newOp, basePath)); + changes.push(...diffResponses(oldOp, newOp, basePath)); + } + } + + return changes; +} + +/** + * Diff operation-level metadata (description, summary, tags, deprecated) + */ +function diffOperationMetadata( + oldOp: OperationMap, + newOp: OperationMap, + basePath: string +): RawChange[] { + const changes: RawChange[] = []; + + // Description changed (patch) + if (oldOp.description !== newOp.description) { + changes.push({ + path: `${basePath}/description`, + type: 'description-changed' as ChangeType, + oldValue: oldOp.description, + newValue: newOp.description, + }); + } + + // Summary changed (patch β€” treated as title) + if (oldOp.summary !== newOp.summary) { + changes.push({ + path: `${basePath}/summary`, + type: 'title-changed' as ChangeType, + oldValue: oldOp.summary, + newValue: newOp.summary, + }); + } + + // Deprecated changed (patch) + if (oldOp.deprecated !== newOp.deprecated) { + changes.push({ + path: `${basePath}/deprecated`, + type: 'deprecated-changed' as ChangeType, + oldValue: oldOp.deprecated, + newValue: newOp.deprecated, + }); + } + + return changes; +} + +/** + * Diff parameters between two operations + */ +function diffParameters(oldOp: OperationMap, newOp: OperationMap, basePath: string): RawChange[] { + const changes: RawChange[] = []; + const oldParams = (oldOp.parameters ?? []) as Array>; + const newParams = (newOp.parameters ?? []) as Array>; + + // Index by "in+name" for matching + const oldByKey = new Map(oldParams.map((p) => [`${p.in}:${p.name}`, p])); + const newByKey = new Map(newParams.map((p) => [`${p.in}:${p.name}`, p])); + + for (const [key, oldParam] of oldByKey) { + if (!newByKey.has(key)) { + changes.push({ + path: `${basePath}/parameters/${oldParam.name}`, + type: 'parameter-removed' as ChangeType, + oldValue: key, + }); + } else { + const newParam = newByKey.get(key)!; + + // Check required change + if (oldParam.required !== newParam.required) { + changes.push({ + path: `${basePath}/parameters/${oldParam.name}/required`, + type: 'parameter-required-changed' as ChangeType, + oldValue: oldParam.required, + newValue: newParam.required, + }); + } + + // Diff parameter schemas using core walker + if (oldParam.schema && newParam.schema) { + const schemaChanges = walk( + oldParam.schema, + newParam.schema, + `${basePath}/parameters/${oldParam.name}/schema` + ); + changes.push(...schemaChanges); + } + } + } + + for (const [key, newParam] of newByKey) { + if (!oldByKey.has(key)) { + // Required parameter added = breaking, optional = non-breaking + // Store required flag in newValue so classifiers can check + changes.push({ + path: `${basePath}/parameters/${newParam.name}`, + type: newParam.required + ? ('parameter-required-added' as ChangeType) + : ('parameter-added' as ChangeType), + newValue: key, + }); + } + } + + return changes; +} + +/** + * Extract the primary schema from a content map (prefers application/json) + */ +function extractSchema(content: Record | undefined): unknown { + if (!content) return undefined; + + // Prefer application/json, then first available + const jsonContent = content['application/json'] as Record | undefined; + if (jsonContent?.schema) return jsonContent.schema; + + for (const mediaType of Object.values(content)) { + const mt = mediaType as Record; + if (mt?.schema) return mt.schema; + } + + return undefined; +} + +/** + * Diff request bodies between two operations + */ +function diffRequestBody(oldOp: OperationMap, newOp: OperationMap, basePath: string): RawChange[] { + const changes: RawChange[] = []; + const oldBody = oldOp.requestBody as Record | undefined; + const newBody = newOp.requestBody as Record | undefined; + + if (oldBody && !newBody) { + changes.push({ + path: `${basePath}/requestBody`, + type: 'request-body-removed' as ChangeType, + }); + } else if (!oldBody && newBody) { + changes.push({ + path: `${basePath}/requestBody`, + type: 'request-body-added' as ChangeType, + }); + } else if (oldBody && newBody) { + const oldSchema = extractSchema(oldBody.content as Record); + const newSchema = extractSchema(newBody.content as Record); + + if (oldSchema && newSchema) { + const schemaChanges = walk(oldSchema, newSchema, `${basePath}/requestBody/content/schema`); + changes.push(...schemaChanges); + } + } + + return changes; +} + +/** + * Diff responses between two operations + */ +function diffResponses(oldOp: OperationMap, newOp: OperationMap, basePath: string): RawChange[] { + const changes: RawChange[] = []; + const oldResponses = (oldOp.responses ?? {}) as Record>; + const newResponses = (newOp.responses ?? {}) as Record>; + + for (const statusCode of Object.keys(oldResponses)) { + if (!(statusCode in newResponses)) { + changes.push({ + path: `${basePath}/responses/${statusCode}`, + type: 'response-removed' as ChangeType, + oldValue: statusCode, + }); + } else { + // Diff response schemas + const oldSchema = extractSchema(oldResponses[statusCode].content as Record); + const newSchema = extractSchema(newResponses[statusCode].content as Record); + + if (oldSchema && newSchema) { + const schemaChanges = walk( + oldSchema, + newSchema, + `${basePath}/responses/${statusCode}/content/schema` + ); + changes.push(...schemaChanges); + } + } + } + + for (const statusCode of Object.keys(newResponses)) { + if (!(statusCode in oldResponses)) { + changes.push({ + path: `${basePath}/responses/${statusCode}`, + type: 'response-added' as ChangeType, + newValue: statusCode, + }); + } + } + + return changes; +} diff --git a/packages/differs.openapi/tsconfig.build.json b/packages/differs.openapi/tsconfig.build.json new file mode 100644 index 0000000..2333a75 --- /dev/null +++ b/packages/differs.openapi/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "exclude": ["node_modules", "dist", "tests", "**/*.test.ts"] +} diff --git a/packages/differs.openapi/tsconfig.json b/packages/differs.openapi/tsconfig.json new file mode 100644 index 0000000..e666b16 --- /dev/null +++ b/packages/differs.openapi/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/generators/contract/.eslintrc b/packages/generators/contract/.eslintrc deleted file mode 100644 index 3280263..0000000 --- a/packages/generators/contract/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../.eslintrc" -} \ No newline at end of file diff --git a/packages/generators/contract/contract.template.hbs b/packages/generators/contract/contract.template.hbs deleted file mode 100644 index 4a96b12..0000000 --- a/packages/generators/contract/contract.template.hbs +++ /dev/null @@ -1,56 +0,0 @@ -import { z } from 'zod'; -import type { AppRouter } from '@ts-rest/core'; -import { initContract } from '@ts-rest/core'; - -{{#if imports}} - {{#each imports}} - import { {{{@key}}} } from "./{{{this}}}" - {{/each}} -{{/if}} - -{{#if schemas}} - {{#each schemas}} - export const {{@key}} = {{{this}}}; - - {{/each}} -{{/if}} - -export const ApiContract = { -{{#each endpoints}} - {{alias}}: { - method: '{{toUpperCase method}}' as const, - path: "{{path}}", - {{#if description}} - description: `{{description}}`, - {{/if}} - {{#if parameters}} - {{#includesType parameters "Path"}} - pathParams: z.object({ - {{#each parameters}}{{#ifeq type "Path"}}'{{name}}': {{{schema}}},{{/ifeq}}{{/each}} - }), - {{/includesType}} - {{#includesType parameters "Query"}} - query: z.object({ - {{#each parameters}}{{#ifeq type "Query"}}'{{name}}': {{{schema}}},{{/ifeq}}{{/each}} - }), - {{/includesType}} - {{#includesType parameters "Body"}} - body: {{#each parameters}}{{#ifeq type "Body"}}{{{schema}}}{{/ifeq}}{{/each}}, - {{/includesType}} - {{/if}} - responses: { - {{#each responses}} - [{{statusCode}}]: {{{schema}}}, - {{/each}} - }, - }, -{{/each}} -} satisfies AppRouter; - -export const ApiOperations = { -{{#each endpoints}} - '{{toPlainWords alias}}': '{{alias}}', -{{/each}} -} satisfies Record; - -export const contract = initContract().router(ApiContract); diff --git a/packages/generators/contract/index.ts b/packages/generators/contract/index.ts deleted file mode 100644 index 8420b10..0000000 --- a/packages/generators/contract/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src'; diff --git a/packages/generators/contract/package.json b/packages/generators/contract/package.json deleted file mode 100644 index 8f12758..0000000 --- a/packages/generators/contract/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@contractual/generators.contract", - "private": false, - "version": "0.0.0", - "license": "MIT", - "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", - "repository": { - "type": "git", - "url": "https://github.com/contractual-dev/contractual.git", - "directory": "packages/generators/client" - }, - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js" - } - }, - "homepage": "https://contractual.dev", - "bugs": { - "url": "https://github.com/contractual-dev/contractual/issues" - }, - "contributors": [ - { - "name": "Omer Morad", - "email": "omer.moradd@gmail.com" - } - ], - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/contractual-dev" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/contractual-dev" - } - ], - "engines": { - "node": ">=18.12.0" - }, - "scripts": { - "prebuild": "pnpm rimraf dist", - "build": "tsc -p tsconfig.build.json", - "build:watch": "tsc -p tsconfig.build.json --watch", - "tester": "jest --coverage --verbose", - "lint": "eslint '{src,test}/**/*.ts'" - }, - "files": [ - "client.template.hbs", - "dist", - "README.md" - ], - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.1", - "chalk": "^5.4.1", - "handlebars": "^4.7.8", - "openapi-types": "^12.1.3", - "openapi-zod-client": "^1.18.2", - "openapi3-ts": "^4.4.0" - }, - "devDependencies": { - "ora": "^8.1.1", - "typescript": "~5.7.2" - }, - "peerDependencies": { - "typescript": ">=5.x" - }, - "publishConfig": { - "access": "public", - "provenance": true - } -} diff --git a/packages/generators/contract/src/command.ts b/packages/generators/contract/src/command.ts deleted file mode 100644 index 05ac4aa..0000000 --- a/packages/generators/contract/src/command.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type ora from 'ora'; -import { ContractCommandHandler, ContractFileGenerator, FileSystemHandler, SpinnerFactory } from './generator.js'; -import * as fs from 'node:fs'; -import SwaggerParser from '@apidevtools/swagger-parser'; -import { generateZodClientFromOpenAPI } from 'openapi-zod-client'; -import { createProgram } from 'typescript'; -import type chalk from 'chalk'; - -export const createContractCommandHandler = ( - spinner: typeof ora, - chalker: typeof chalk, - logger: Console, - apiSpecPath: string -) => { - const spinnerFactory = new SpinnerFactory(spinner); - - const fileGenerator = new ContractFileGenerator( - spinnerFactory, - generateZodClientFromOpenAPI, - createProgram, - new FileSystemHandler(fs) - ); - - return new ContractCommandHandler( - spinnerFactory, - new FileSystemHandler(fs), - SwaggerParser, - fileGenerator, - logger, - chalker, - apiSpecPath - ); -}; diff --git a/packages/generators/contract/src/generator.ts b/packages/generators/contract/src/generator.ts deleted file mode 100644 index d98173c..0000000 --- a/packages/generators/contract/src/generator.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type SwaggerParser from '@apidevtools/swagger-parser'; -import * as path from 'node:path'; -import type { generateZodClientFromOpenAPI } from 'openapi-zod-client'; -import type { OpenAPIObject } from 'openapi3-ts/oas30'; -import * as process from 'node:process'; -import type { createProgram } from 'typescript'; -import { ModuleKind, ModuleResolutionKind, ScriptTarget } from 'typescript'; -import { createHandlebars } from './handlebars-helpers.js'; -import type ora from 'ora'; -import type * as fs from 'node:fs'; -import type chalk from 'chalk'; - -export class SpinnerFactory { - public constructor(private readonly spinner: typeof ora) {} - - public createSpinner(text: string) { - return this.spinner(text); - } -} - -export class FileSystemHandler { - public constructor(private readonly fileSystem: typeof fs) {} - - public exists(path: string): boolean { - return this.fileSystem.existsSync(path); - } - - public removeFile(path: string): void { - this.fileSystem.rmSync(path); - } -} - -export class ContractFileGenerator { - public constructor( - private readonly spinnerFactory: SpinnerFactory, - private readonly zodGenerator: typeof generateZodClientFromOpenAPI, - private readonly programFactory: typeof createProgram, - private readonly fileSystemHandler: FileSystemHandler - ) {} - - public async generateFiles(openapiDocument: OpenAPIObject, destinationPath: string) { - const spinner = this.spinnerFactory.createSpinner('Generating contract files..').start(); - const indexTsDestinationPath = path.resolve(destinationPath, 'index.ts'); - - try { - await this.zodGenerator({ - distPath: indexTsDestinationPath, - openApiDoc: openapiDocument, - templatePath: path.resolve( - path.dirname(new URL(import.meta.url).pathname), - '..', - 'contract.template.hbs' - ), - prettierConfig: { - tabWidth: 2, - semi: true, - singleQuote: true, - trailingComma: 'all', - bracketSpacing: true, - arrowParens: 'always', - }, - options: { - additionalPropertiesDefaultValue: false, - withAllResponses: true, - withAlias: true, - withDescription: false, - withDefaultValues: false, - }, - handlebars: createHandlebars(), - }); - - spinner.succeed('Generated Contractual contract'); - return indexTsDestinationPath; - } catch (error) { - spinner.fail('Failed to generate contract files.'); - throw error; - } - } - - public async compileFiles(destinationPath: string) { - const spinner = this.spinnerFactory.createSpinner('Compiling contract..').start(); - const indexTsDestinationPath = path.resolve(destinationPath, 'index.ts'); - - try { - this.programFactory([indexTsDestinationPath], { - module: ModuleKind.ESNext, - target: ScriptTarget.ESNext, - skipLibCheck: true, - declaration: true, - noImplicitAny: true, - moduleResolution: ModuleResolutionKind.Bundler, - outDir: destinationPath, - }).emit(); - - spinner.succeed('Compilation completed successfully.'); - this.fileSystemHandler.removeFile(indexTsDestinationPath); - } catch (error) { - spinner.fail('Failed to compile contract.'); - throw error; - } - } -} - -export class ContractCommandHandler { - public constructor( - private readonly spinnerFactory: SpinnerFactory, - private readonly fileSystemHandler: FileSystemHandler, - private readonly swaggerParser: typeof SwaggerParser, - private readonly fileGenerator: ContractFileGenerator, - private readonly logger: Console, - private readonly chalker: typeof chalk, - private readonly apiSpecPathToReadFrom: string - ) {} - - public async handle() { - try { - if (!this.fileSystemHandler.exists(this.apiSpecPathToReadFrom)) { - this.logError( - 'Could not find Contractual schema that is required for this command. You can provide it with `--spec` argument' - ); - - return; - } - - this.logger.log( - this.chalker.gray(`Contractual schema loaded from ${this.apiSpecPathToReadFrom}`) - ); - - const openapiDocument = await this.parseSpec(this.apiSpecPathToReadFrom); - - const destinationPath = path.resolve( - path.dirname(new URL(import.meta.url).pathname), - '../../..', - 'contract/contract' - ); - - await this.fileGenerator.generateFiles(openapiDocument, destinationPath); - await this.fileGenerator.compileFiles(destinationPath); - - this.logger.log(this.chalker.green('Done')); - } catch (error) { - this.logError('Error occurred during contract generation.', error); - } - } - - private async parseSpec(apiSpecPath: string): Promise { - const spinner = this.spinnerFactory.createSpinner('Parsing TypeSpec file..').start(); - - return this.swaggerParser - .parse(apiSpecPath) - .then((document) => { - if (!('openapi' in document)) { - throw new Error('Invalid OpenAPI document: Missing "openapi" property.'); - } - - spinner.succeed('TypeSpec file parsed successfully.'); - - return document as OpenAPIObject; - }) - .catch((error) => { - spinner.fail('Failed to parse TypeSpec file.'); - - throw error instanceof Error ? new Error(error.message) : error; - }); - } - - private logError(message: string, error?: unknown) { - this.logger.log(`${this.chalker.red('Error')}: ${message}`); - - if (error) { - this.logger.error('Error details:', error); - } - } -} diff --git a/packages/generators/contract/src/handlebars-helpers.ts b/packages/generators/contract/src/handlebars-helpers.ts deleted file mode 100644 index 506c2e3..0000000 --- a/packages/generators/contract/src/handlebars-helpers.ts +++ /dev/null @@ -1,51 +0,0 @@ -import handlebars from 'handlebars'; - -const { create } = handlebars; - -export function createHandlebars() { - const instance = create(); - - instance.registerHelper('ifNotEmptyObj', function (obj, options) { - if (typeof obj === 'object' && Object.keys(obj).length > 0) { - return options.fn(this); - } - - return options.inverse(this); - }); - - instance.registerHelper('toUpperCase', (input: string) => input.toUpperCase()); - - instance.registerHelper('ifNotEq', function (a, b, options) { - if (a !== b) { - return options.fn(this); - } - - return options.inverse(this); - }); - - instance.registerHelper('ifeq', function (a, b, options) { - if (a === b) { - return options.fn(this); - } - - return options.inverse(this); - }); - - instance.registerHelper('includesType', function (arr, val, options) { - if (Array.isArray(arr) && arr.length > 0 && arr.some((v) => v.type === val)) { - return options.fn(this); - } - - return options.inverse(this); - }); - - instance.registerHelper('toPlainWords', function (str) { - return str.replace(/([A-Z])/g, ' $1').toLowerCase(); - }); - - instance.registerHelper('toDashes', function (str) { - return str.replace(/([A-Z])/g, '-$1').toLowerCase(); - }); - - return instance; -} diff --git a/packages/generators/contract/src/index.ts b/packages/generators/contract/src/index.ts deleted file mode 100644 index 4e2ea87..0000000 --- a/packages/generators/contract/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './command.js'; diff --git a/packages/generators/contract/tsconfig.build.json b/packages/generators/contract/tsconfig.build.json deleted file mode 100644 index 0bc65ae..0000000 --- a/packages/generators/contract/tsconfig.build.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.build.json", - "compilerOptions": { - "rootDir": "src", - "baseUrl": ".", - "outDir": "dist", - "skipLibCheck": true - }, - "exclude": [ - "dist", - "index.ts", - "node_modules", - "test", - "**/*.spec.ts", - "jest.config.ts" - ] -} \ No newline at end of file diff --git a/packages/generators/contract/tsconfig.json b/packages/generators/contract/tsconfig.json deleted file mode 100644 index b027bb4..0000000 --- a/packages/generators/contract/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} \ No newline at end of file diff --git a/packages/generators/diff/.eslintrc b/packages/generators/diff/.eslintrc deleted file mode 100644 index 3280263..0000000 --- a/packages/generators/diff/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../.eslintrc" -} \ No newline at end of file diff --git a/packages/generators/diff/index.ts b/packages/generators/diff/index.ts deleted file mode 100644 index 8420b10..0000000 --- a/packages/generators/diff/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src'; diff --git a/packages/generators/diff/package.json b/packages/generators/diff/package.json deleted file mode 100644 index f09cb42..0000000 --- a/packages/generators/diff/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "@contractual/generators.diff", - "private": false, - "version": "0.0.0", - "license": "MIT", - "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", - "repository": { - "type": "git", - "url": "https://github.com/contractual-dev/contractual.git", - "directory": "packages/generators/diff" - }, - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js" - } - }, - "homepage": "https://contractual.dev", - "bugs": { - "url": "https://github.com/contractual-dev/contractual/issues" - }, - "contributors": [ - { - "name": "Omer Morad", - "email": "omer.moradd@gmail.com" - } - ], - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/contractual-dev" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/contractual-dev" - } - ], - "engines": { - "node": ">=18.12.0" - }, - "scripts": { - "prebuild": "pnpm rimraf dist", - "build": "tsc -p tsconfig.build.json", - "test": "pnpm vitest run --watch", - "build:watch": "tsc -p tsconfig.build.json --watch", - "lint": "eslint '{src,test}/**/*.ts'" - }, - "files": [ - "dist", - "README.md" - ], - "publishConfig": { - "access": "public", - "provenance": true - }, - "dependencies": { - "chalk": "^5.4.1", - "openapi-diff": "^0.23.7", - "openapi-types": "^12.1.3", - "semver": "^7.6.3", - "table": "^6.9.0" - }, - "devDependencies": { - "@types/semver": "^7.5.8", - "@vitest/coverage-c8": "^0.33.0", - "typescript": "~5.7.2", - "vitest": "^3.0.3" - } -} diff --git a/packages/generators/diff/src/diff-highlighter.ts b/packages/generators/diff/src/diff-highlighter.ts deleted file mode 100644 index 58a281b..0000000 --- a/packages/generators/diff/src/diff-highlighter.ts +++ /dev/null @@ -1,77 +0,0 @@ -import chalk from 'chalk'; -import { table } from 'table'; -import type OpenApiDiff from 'openapi-diff'; -import { type DiffOutcome } from 'openapi-diff'; -import type { TableUserConfig } from 'table/dist/src/types/api.js'; - -async function printOpenApiDiff(diffOutcome: DiffOutcome) { - try { - if ('breakingDifferencesFound' in diffOutcome && diffOutcome.breakingDifferencesFound) { - console.log(chalk.red.bold('\nBreaking Differences Found:\n')); - console.log(formatDiffs(diffOutcome.breakingDifferences, chalk.red)); - } - - if (diffOutcome.nonBreakingDifferences.length > 0) { - console.log(chalk.green.bold('\nNon-Breaking Differences:\n')); - console.log(formatDiffs(diffOutcome.nonBreakingDifferences, chalk.green)); - } - - if (diffOutcome.unclassifiedDifferences.length > 0) { - console.log(chalk.yellow.bold('\nUnclassified Differences:\n')); - console.log(formatDiffs(diffOutcome.unclassifiedDifferences, chalk.yellow)); - } - - if ( - !('breakingDifferencesFound' in diffOutcome) || - (!diffOutcome.breakingDifferencesFound && - diffOutcome.nonBreakingDifferences.length === 0 && - diffOutcome.unclassifiedDifferences.length === 0) - ) { - console.log( - chalk.green.bold('\nNo Differences Found! Specifications are identical or compatible.\n') - ); - } - } catch (error) { - console.error(chalk.red.bold('\nError performing diff:\n')); - console.error(error); - } -} - -function formatDiffs( - diffs: OpenApiDiff.DiffResult[], - colorFn: (text: string) => string -): string { - const formattedDiffs = diffs.map((diff) => [ - colorFn(diff.code), - colorFn(diff.entity), - chalk.bold(diff.action === 'add' ? 'βž• Add' : '❌ Remove'), - diff.sourceSpecEntityDetails.map((d) => `${d.location}`).join(', '), - diff.destinationSpecEntityDetails.map((d) => `${d.location}`).join(', '), - ]); - - const tableConfig: TableUserConfig = { - columns: { - 0: { alignment: 'left', width: 20 }, - 1: { alignment: 'left', width: 20 }, - 2: { alignment: 'center', width: 10 }, - 3: { alignment: 'left', width: 30 }, - 4: { alignment: 'left', width: 30 }, - }, - }; - - return table( - [ - [ - chalk.underline.bold('Code'), - chalk.underline.bold('Entity'), - chalk.underline.bold('Action'), - chalk.underline.bold('Source Location'), - chalk.underline.bold('Destination Location'), - ], - ...formattedDiffs, - ], - tableConfig - ); -} - -export { printOpenApiDiff }; diff --git a/packages/generators/diff/src/generator.spec.ts b/packages/generators/diff/src/generator.spec.ts deleted file mode 100644 index 324ad41..0000000 --- a/packages/generators/diff/src/generator.spec.ts +++ /dev/null @@ -1,886 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { diffSpecs } from './generator'; - -const dummyOpenApiSpecBreaking = ` -openapi: 3.0.0 -info: - title: Petstore API - description: A simple API for managing a pet store. - version: 0.0.0 -tags: [] -paths: - /pet: - post: - operationId: addPet - description: Add a new pet to the store. - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreatePetResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AddPetBody' - /pet/{petId}: - get: - operationId: getPetById - description: Retrieve a pet by ID. - parameters: - - name: petId - in: path - required: true - schema: - type: integer - format: int32 - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetPetResponse' - /store/order: - post: - operationId: placeOrder - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrderResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PlaceOrderBody' - /store/order/{orderId}: - get: - operationId: getOrderById - description: Retrieve an order by ID. - parameters: - - name: orderId - in: path - required: true - schema: - type: integer - format: int32 - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrderResponse' - /user: - post: - operationId: createUser - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserBody' - /user/{username}: - get: - operationId: getUserByUsername - description: Retrieve a user by username. - parameters: - - name: username - in: path - required: true - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetUserResponse' -components: - schemas: - AddPetBody: - type: object - required: - - name - properties: - name: - type: string - description: Name of the pet. - category: - type: string - description: Category of the pet (e.g., dog, cat, bird). - tags: - type: array - items: - type: string - description: List of tags associated with the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - description: Request body for adding a new pet. - ApiResponse: - type: object - required: - - code - - type - - message - properties: - code: - type: integer - format: int32 - description: Status code of the response. - type: - type: string - description: The type of response (e.g., success, error). - message: - type: string - description: Detailed message about the response. - description: Represents a standard API response for messages or errors. - CreateOrderResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created order. - description: Response for creating an order. - CreatePetResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created pet. - description: Response for creating a pet. - CreateUserBody: - type: object - required: - - username - - firstName - - lastName - - email - - password - - phone - properties: - username: - type: string - description: Username of the user. - firstName: - type: string - description: First name of the user. - lastName: - type: string - description: Last name of the user. - email: - type: string - description: Email address of the user. - password: - type: string - description: Password for the user account. - phone: - type: string - description: Phone number of the user. - userStatus: - type: integer - format: int32 - description: User status (e.g., 1 for active, 0 for inactive). - description: Create a new user. - CreateUserResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created user. - description: Response for creating a user. - GetOrderResponse: - type: object - required: - - id - - petId - - status - - quantity - properties: - id: - type: integer - format: int32 - description: Unique identifier of the order. - petId: - type: integer - format: int32 - description: Identifier of the pet associated with the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - description: Response for retrieving an order. - GetPetResponse: - type: object - required: - - id - - name - - status - properties: - id: - type: integer - format: int32 - description: Unique identifier of the pet. - name: - type: string - description: Name of the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - description: Response for retrieving a pet. - GetUserResponse: - type: object - required: - - id - - username - - email - - phone - properties: - id: - type: integer - format: int32 - description: Unique identifier of the user. - username: - type: string - description: Username associated with the user. - email: - type: string - description: Email address of the user. - phone: - type: string - description: Phone number of the user. - description: Response for retrieving a user. - Order: - type: object - required: - - id - - petId - - quantity - - shipDate - - status - properties: - id: - type: integer - format: int32 - description: Unique identifier for the order. - petId: - type: integer - format: int32 - description: ID of the pet being ordered. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - shipDate: - type: string - description: Shipping date for the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - complete: - type: boolean - description: Indicates whether the order is complete. - description: Represents an order for purchasing a pet. - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int32 - description: Unique identifier for the pet. - name: - type: string - description: Name of the pet. - category: - type: string - description: Category of the pet (e.g., dog, cat, bird). - tags: - type: array - items: - type: string - description: List of tags associated with the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - description: Represents a pet in the store. - PlaceOrderBody: - type: object - required: - - petId - - quantity - - shipDate - - status - properties: - petId: - type: integer - format: int32 - description: ID of the pet being ordered. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - shipDate: - type: string - description: Shipping date for the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - complete: - type: boolean - description: Indicates whether the order is complete. - description: Place an order for a pet. - User: - type: object - required: - - id - - username - - firstName - - lastName - - email - - password - - phone - properties: - id: - type: integer - format: int32 - description: Unique identifier for the user. - username: - type: string - description: Username of the user. - firstName: - type: string - description: First name of the user. - lastName: - type: string - description: Last name of the user. - email: - type: string - description: Email address of the user. - password: - type: string - description: Password for the user account. - phone: - type: string - description: Phone number of the user. - userStatus: - type: integer - format: int32 - description: User status (e.g., 1 for active, 0 for inactive). - description: Represents a user of the pet store. -`; - -const dummyOpenApiSpecMinor = ` -openapi: 3.0.0 -info: - title: Petstore API - description: A simple API for managing a pet store. - version: 0.0.0 -tags: [] -paths: - /pet: - post: - operationId: addPet - description: Add a new pet to the store. - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreatePetResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AddPetBody' - /pet/{petId}: - get: - operationId: getPetById - description: Retrieve a pet by ID. - parameters: - - name: petId - in: path - required: true - schema: - type: integer - format: int32 - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetPetResponse' - /store/order: - post: - operationId: placeOrder - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateOrderResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PlaceOrderBody' - /store/order/{orderId}: - get: - operationId: getOrderById - description: Retrieve an order by ID. - parameters: - - name: orderId - in: path - required: true - schema: - type: integer - format: int32 - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetOrderResponse' - /user: - post: - operationId: createUser - parameters: [] - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserResponse' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserBody' - /user/{username}: - get: - operationId: getUserByUsername - description: Retrieve a user by username. - parameters: - - name: username - in: path - required: true - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/GetUserResponse' -components: - schemas: - AddPetBody: - type: object - required: - - name - properties: - name: - type: string - description: Name of the pet. - category: - type: string - description: Category of the pet (e.g., dog, cat, bird). - tags: - type: array - items: - type: string - description: List of tags associated with the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - description: Request body for adding a new pet. - ApiResponse: - type: object - required: - - code - - type - - message - properties: - code: - type: integer - format: int32 - description: Status code of the response. - type: - type: string - description: The type of response (e.g., success, error). - message: - type: string - description: Detailed message about the response. - description: Represents a standard API response for messages or errors. - CreateOrderResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created order. - description: Response for creating an order. - CreatePetResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created pet. - description: Response for creating a pet. - CreateUserBody: - type: object - required: - - username - - firstName - - lastName - - email - - password - - phone - properties: - username: - type: string - description: Username of the user. - firstName: - type: string - description: First name of the user. - lastName: - type: string - description: Last name of the user. - email: - type: string - description: Email address of the user. - password: - type: string - description: Password for the user account. - phone: - type: string - description: Phone number of the user. - userStatus: - type: integer - format: int32 - description: User status (e.g., 1 for active, 0 for inactive). - description: Create a new user. - CreateUserResponse: - type: object - required: - - id - properties: - id: - type: integer - format: int32 - description: Unique identifier of the created user. - description: Response for creating a user. - GetOrderResponse: - type: object - required: - - id - - petId - - status - - quantity - properties: - id: - type: integer - format: int32 - description: Unique identifier of the order. - petId: - type: integer - format: int32 - description: Identifier of the pet associated with the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - description: Response for retrieving an order. - GetPetResponse: - type: object - required: - - id - - name - - status - properties: - id: - type: integer - format: int32 - description: Unique identifier of the pet. - name: - type: string - description: Name of the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - category: - type: string - description: Category of the pet (e.g., dog, cat, bird). - nickname: - type: string - description: Category of the pet (e.g., dog, cat, bird). - description: Response for retrieving a pet. - GetUserResponse: - type: object - required: - - id - - username - - email - - phone - properties: - id: - type: integer - format: int32 - description: Unique identifier of the user. - username: - type: string - description: Username associated with the user. - email: - type: string - description: Email address of the user. - phone: - type: string - description: Phone number of the user. - description: Response for retrieving a user. - Order: - type: object - required: - - id - - petId - - quantity - - shipDate - - status - properties: - id: - type: integer - format: int32 - description: Unique identifier for the order. - petId: - type: integer - format: int32 - description: ID of the pet being ordered. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - shipDate: - type: string - description: Shipping date for the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - complete: - type: boolean - description: Indicates whether the order is complete. - description: Represents an order for purchasing a pet. - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int32 - description: Unique identifier for the pet. - name: - type: string - description: Name of the pet. - category: - type: string - description: Category of the pet (e.g., dog, cat, bird). - tags: - type: array - items: - type: string - description: List of tags associated with the pet. - status: - type: string - enum: - - available - - pending - - sold - description: The current status of the pet in the store. - description: Represents a pet in the store. - PlaceOrderBody: - type: object - required: - - petId - - quantity - - shipDate - - status - properties: - petId: - type: integer - format: int32 - description: ID of the pet being ordered. - quantity: - type: integer - format: int32 - description: Quantity of pets ordered. - shipDate: - type: string - description: Shipping date for the order. - status: - type: string - enum: - - placed - - approved - - delivered - description: The current status of the order. - complete: - type: boolean - description: Indicates whether the order is complete. - description: Place an order for a pet. - User: - type: object - required: - - id - - username - - firstName - - lastName - - email - - password - - phone - properties: - id: - type: integer - format: int32 - description: Unique identifier for the user. - username: - type: string - description: Username of the user. - firstName: - type: string - description: First name of the user. - lastName: - type: string - description: Last name of the user. - email: - type: string - description: Email address of the user. - password: - type: string - description: Password for the user account. - phone: - type: string - description: Phone number of the user. - userStatus: - type: integer - format: int32 - description: User status (e.g., 1 for active, 0 for inactive). - description: Represents a user of the pet store. -`; - -describe('diffSpecs', () => { - const tempDir = path.resolve(new URL('.', import.meta.url).pathname, 'temp'); - const spec1Path = path.join(tempDir, 'spec1.yaml'); - const spec2Path = path.join(tempDir, 'spec2.yaml'); - - beforeAll(() => { - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir); - } - }); - - afterAll(() => { - fs.rmSync(tempDir, { force: true, recursive: true }); - }); - - it.only('should detect no changes between identical specs', async () => { - fs.writeFileSync(spec1Path, dummyOpenApiSpecBreaking); - fs.writeFileSync(spec2Path, dummyOpenApiSpecMinor); - const result = await diffSpecs(spec2Path, spec1Path); - expect(result.version).toBe('minor'); - expect(result.diff).toEqual([]); - }); -}); diff --git a/packages/generators/diff/src/generator.ts b/packages/generators/diff/src/generator.ts deleted file mode 100644 index 97635f7..0000000 --- a/packages/generators/diff/src/generator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as fs from 'node:fs'; -import openapiDiff, { type DiffOutcome } from 'openapi-diff'; - -interface DiffResult { - version: 'minor' | 'major' | 'patch' | 'unchanged'; - diff: DiffOutcome | null; -} - -export async function diffSpecs( - destinationSpecPath: string, - sourceSpecPath: string -): Promise { - const diff = await openapiDiff.diffSpecs({ - destinationSpec: { - content: fs.readFileSync(destinationSpecPath, 'utf-8'), - location: destinationSpecPath, - format: 'openapi3', - }, - sourceSpec: { - content: fs.readFileSync(sourceSpecPath, 'utf-8'), - location: sourceSpecPath, - format: 'openapi3', - }, - }); - - if (diff.breakingDifferencesFound) { - return { version: 'major', diff: diff }; - } - - if (diff.unclassifiedDifferences.length === 0 && diff.nonBreakingDifferences.length === 0) { - return { version: 'unchanged', diff: null }; - } - - return { version: 'minor', diff: diff }; -} diff --git a/packages/generators/diff/src/index.ts b/packages/generators/diff/src/index.ts deleted file mode 100644 index ff776ac..0000000 --- a/packages/generators/diff/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './generator.js'; -export * from './diff-highlighter.js'; diff --git a/packages/generators/diff/tsconfig.build.json b/packages/generators/diff/tsconfig.build.json deleted file mode 100644 index 0bc65ae..0000000 --- a/packages/generators/diff/tsconfig.build.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.build.json", - "compilerOptions": { - "rootDir": "src", - "baseUrl": ".", - "outDir": "dist", - "skipLibCheck": true - }, - "exclude": [ - "dist", - "index.ts", - "node_modules", - "test", - "**/*.spec.ts", - "jest.config.ts" - ] -} \ No newline at end of file diff --git a/packages/generators/diff/tsconfig.json b/packages/generators/diff/tsconfig.json deleted file mode 100644 index b027bb4..0000000 --- a/packages/generators/diff/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} \ No newline at end of file diff --git a/packages/generators/spec/.eslintrc b/packages/generators/spec/.eslintrc deleted file mode 100644 index 3280263..0000000 --- a/packages/generators/spec/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../.eslintrc" -} \ No newline at end of file diff --git a/packages/generators/spec/index.ts b/packages/generators/spec/index.ts deleted file mode 100644 index 8420b10..0000000 --- a/packages/generators/spec/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src'; diff --git a/packages/generators/spec/package.json b/packages/generators/spec/package.json deleted file mode 100644 index 825caca..0000000 --- a/packages/generators/spec/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "@contractual/generators.spec", - "private": false, - "version": "0.0.0", - "license": "MIT", - "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", - "repository": { - "type": "git", - "url": "https://github.com/contractual-dev/contractual.git", - "directory": "packages/generators/spec" - }, - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.js" - } - }, - "homepage": "https://contractual.dev", - "bugs": { - "url": "https://github.com/contractual-dev/contractual/issues" - }, - "contributors": [ - { - "name": "Omer Morad", - "email": "omer.moradd@gmail.com" - } - ], - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/contractual-dev" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/contractual-dev" - } - ], - "engines": { - "node": ">=18.12.0" - }, - "scripts": { - "prebuild": "pnpm rimraf dist", - "build": "tsc -p tsconfig.build.json", - "build:watch": "tsc -p tsconfig.build.json --watch", - "tester": "jest --coverage --verbose", - "lint": "eslint '{src,test}/**/*.ts'" - }, - "files": [ - "dist", - "README.md" - ], - "publishConfig": { - "access": "public", - "provenance": true - }, - "dependencies": { - "@contractual/generators.diff": "workspace:*", - "@typespec/compiler": "^0.63.0", - "@typespec/http": "^0.63.0", - "@typespec/openapi": "^0.63.0", - "@typespec/openapi3": "^0.63.0", - "@typespec/rest": "^0.63.1", - "@typespec/versioning": "^0.63.0", - "semver": "^7.6.3", - "yaml": "^2.7.0" - }, - "devDependencies": { - "@types/semver": "^7.5.8", - "chalk": "^5.4.1", - "inquirer": "^12.3.2", - "openapi-types": "^12.1.3", - "ora": "^8.1.1", - "typescript": "~5.7.2" - }, - "peerDependencies": { - "typescript": ">=5.x" - } -} diff --git a/packages/generators/spec/src/generator.ts b/packages/generators/spec/src/generator.ts deleted file mode 100644 index 415c74a..0000000 --- a/packages/generators/spec/src/generator.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { compile, logDiagnostics, NodeHost } from '@typespec/compiler'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as process from 'node:process'; -import { parse, stringify } from 'yaml'; -import { inc } from 'semver'; -import type inquirer from 'inquirer'; -import ora from 'ora'; -import chalk from 'chalk'; -import { diffSpecs, printOpenApiDiff } from '@contractual/generators.diff'; - -export function initializePaths() { - const rootPath = path.resolve(process.cwd(), 'contractual'); - const configFilePath = path.resolve(rootPath, 'api-lock.yaml'); - const snapshotsPath = path.resolve(rootPath, 'specs'); - const currentPath = path.resolve(path.dirname(new URL(import.meta.url).pathname)); - const specPath = path.resolve(rootPath, 'api.tsp'); - const tempSpecPath = path.resolve(currentPath, '@typespec', 'openapi3', 'openapi.yaml'); - - return { rootPath, configFilePath, snapshotsPath, currentPath, specPath, tempSpecPath }; -} - -function checkFileExists(filePath: string, errorMessage: string): boolean { - if (!fs.existsSync(filePath)) { - console.error(errorMessage, filePath); - return false; - } - - return true; -} - -async function compileSpecification(specPath: string, outputPath: string) { - const program = await compile(NodeHost, specPath, { - emit: ['@typespec/openapi3'], - additionalImports: ['@typespec/openapi', '@typespec/openapi3', '@typespec/http'], - outputDir: outputPath, - ignoreDeprecated: true, - warningAsError: false, - }); - - if (program.hasError()) { - logDiagnostics( - program.diagnostics.filter(({ severity }) => severity === 'error'), - NodeHost.logSink - ); - - return null; - } - - return program; -} - -async function checkSpecificationDifferences( - tempSpecPath: string, - snapshotsPath: string, - version: string -) { - return diffSpecs(tempSpecPath, path.resolve(snapshotsPath, `openapi-v${version}.yaml`)); -} - -function updateVersionAndSnapshot( - configPath: string, - snapshotsPath: string, - tempSpecPath: string, - currentVersion: string -) { - const newVersion = inc(currentVersion, 'minor'); - const newConfigContent = stringify({ version: { latest: newVersion } }); - - fs.writeFileSync(configPath, newConfigContent); - fs.copyFileSync(tempSpecPath, path.resolve(snapshotsPath, `openapi-v${newVersion}.yaml`)); -} - -export function getLatestVersion(configPath: string) { - const configContent = parse(fs.readFileSync(configPath, 'utf-8')); - return configContent.version.latest; -} - -export async function generateSpecification(inquirerDep: typeof inquirer) { - const paths = initializePaths(); - - if (!checkFileExists(paths.rootPath, `'contractual' directory not found`)) { - return; - } - - if (!fs.existsSync(paths.snapshotsPath)) { - fs.mkdirSync(paths.snapshotsPath); - } - - if (!checkFileExists(paths.specPath, 'specification file not found')) { - process.exit(1); - } - - const latest = getLatestVersion(paths.configFilePath); - - console.log(chalk.gray(`Latest version is ${latest}`)); - - const spinner = ora('Compiling TypeSpec API specification..').start(); - - const program = await compileSpecification(paths.specPath, paths.currentPath); - - if (!program) { - spinner.fail('Compilation failed due to compilation errors'); - return; - } - - if (!checkFileExists(paths.tempSpecPath, 'openapi.yaml not found')) { - spinner.fail('Compilation failed due to missing temp openapi.yaml'); - return; - } - - spinner.succeed('TypeSpec API specification compiled successfully'); - - if (latest === 'unversioned') { - const { initialVersion } = await inquirerDep.prompt([ - { - type: 'input', - name: 'initialVersion', - message: 'Please provide the initial version (e.g., 1.0.0):', - default: '1.0.0', - validate: (input) => - /^\d+\.\d+\.\d+$/.test(input) || - 'Invalid version format. Please use semantic versioning format (e.g., 1.0.0).', - }, - ]); - - const updateSpinner = ora('Creating new version..').start(); - - const destinationPath = path.resolve(paths.snapshotsPath, `openapi-v${initialVersion}.yaml`); - - fs.copyFileSync(paths.tempSpecPath, destinationPath); - - updateSpinner.info(`New version ${initialVersion} created successfully`); - - const newConfigContent = stringify({ version: { latest: initialVersion } }); - - fs.writeFileSync(paths.configFilePath, newConfigContent); - - updateSpinner.info(`Updated to new version: ${initialVersion}`); - - updateSpinner.succeed('Specification generated successfully'); - - return; - } - - const diffSpinner = ora('Checking for API diff..').start(); - - const differences = await checkSpecificationDifferences( - paths.tempSpecPath, - paths.snapshotsPath, - latest - ); - - if (differences.version === 'unchanged') { - diffSpinner.info('No differences found. Specifications are identical or compatible.'); - return; - } - - diffSpinner.info('Differences found'); - - await printOpenApiDiff(differences.diff!); - - const { confirmUpdate } = await inquirerDep.prompt([ - { - type: 'confirm', - name: 'confirmUpdate', - message: 'Do you want to update the API version?', - default: true, - }, - ]); - - if (!confirmUpdate) { - diffSpinner.info('API version not updated'); - return; - } - - diffSpinner.start('Updating API version..'); - - updateVersionAndSnapshot(paths.configFilePath, paths.snapshotsPath, paths.tempSpecPath, latest); - - diffSpinner.succeed('API version updated successfully'); -} diff --git a/packages/generators/spec/src/index.ts b/packages/generators/spec/src/index.ts deleted file mode 100644 index 9f33372..0000000 --- a/packages/generators/spec/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './generator.js'; diff --git a/packages/generators/spec/tsconfig.build.json b/packages/generators/spec/tsconfig.build.json deleted file mode 100644 index 0bc65ae..0000000 --- a/packages/generators/spec/tsconfig.build.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.build.json", - "compilerOptions": { - "rootDir": "src", - "baseUrl": ".", - "outDir": "dist", - "skipLibCheck": true - }, - "exclude": [ - "dist", - "index.ts", - "node_modules", - "test", - "**/*.spec.ts", - "jest.config.ts" - ] -} \ No newline at end of file diff --git a/packages/generators/spec/tsconfig.json b/packages/generators/spec/tsconfig.json deleted file mode 100644 index b027bb4..0000000 --- a/packages/generators/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} \ No newline at end of file diff --git a/packages/governance/CHANGELOG.md b/packages/governance/CHANGELOG.md new file mode 100644 index 0000000..c0c961e --- /dev/null +++ b/packages/governance/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/governance@0.1.0-dev.7...@contractual/governance@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* address security findings on next ([#19](https://github.com/codotech/contractual/issues/19)) ([2466cd3](https://github.com/codotech/contractual/commit/2466cd3593fa093b3ec07eae22b6c248268fc8b9)) +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) +* remediate dependency and code scanning vulnerabilities ([#18](https://github.com/codotech/contractual/issues/18)) ([ee6d9a3](https://github.com/codotech/contractual/commit/ee6d9a3a07f2b854505cd1dea815cbefccdc5592)) diff --git a/packages/governance/differs/index.ts b/packages/governance/differs/index.ts new file mode 100644 index 0000000..d41479f --- /dev/null +++ b/packages/governance/differs/index.ts @@ -0,0 +1,12 @@ +/** + * Differs + * + * Contract differ implementations for detecting structural changes + * between two versions of a contract specification. + * + * All differs are pure Node.js - no binary dependencies. + */ + +export { diffOpenApi, diffOpenApiObjects, resolveOpenApiSpec } from '@contractual/differs.openapi'; + +export { diffJsonSchema } from './json-schema/index.js'; diff --git a/packages/governance/differs/json-schema/index.ts b/packages/governance/differs/json-schema/index.ts new file mode 100644 index 0000000..8fc70fa --- /dev/null +++ b/packages/governance/differs/json-schema/index.ts @@ -0,0 +1,9 @@ +/** + * JSON Schema Differ + * + * Re-exports from @contractual/differs.json-schema package. + * This module provides JSON Schema structural diffing capabilities. + */ + +// Re-export everything from the json-schema-differ package +export * from '@contractual/differs.json-schema'; diff --git a/packages/governance/index.ts b/packages/governance/index.ts new file mode 100644 index 0000000..5014918 --- /dev/null +++ b/packages/governance/index.ts @@ -0,0 +1,106 @@ +/** + * Governance Engines + * + * Central module for all linting and diffing capabilities. + * Registers all built-in engines with the registry at import time. + */ + +import { registerLinter, registerDiffer } from './registry.js'; +import { lintOpenAPI } from './linters/openapi-redocly.js'; +import { lintJsonSchema } from './linters/json-schema-ajv.js'; +import { diffOpenApi } from '@contractual/differs.openapi'; +import { diffJsonSchema } from '@contractual/differs.json-schema'; + +// Re-export registry functions +export { + registerLinter, + registerDiffer, + getLinter, + getDiffer, + hasLinter, + hasDiffer, + getRegisteredLinterTypes, + getRegisteredDifferTypes, +} from './registry.js'; + +// Re-export types from @contractual/types +export type { + Change, + ChangeSeverity, + DiffResult, + DiffSummary, + SuggestedBump, + LintResult, + LintIssue, + LintSeverity, + LintFn, + DiffFn, + ContractType, + RawChange, + ChangeType, +} from '@contractual/types'; + +// Re-export linters +export { lintOpenAPI } from './linters/openapi-redocly.js'; +export { lintJsonSchema } from './linters/json-schema-ajv.js'; + +// Re-export OpenAPI differ +export { diffOpenApi, diffOpenApiObjects, resolveOpenApiSpec } from '@contractual/differs.openapi'; + +// Re-export everything from JSON Schema differ package +export { + diffJsonSchema, + diffJsonSchemaObjects, + compareSchemas, + checkCompatibility, + classify, + classifyPropertyAdded, + classifyAll, + CLASSIFICATION_SETS, + resolveRefs, + hasUnresolvedRefs, + extractRefs, + validateRefs, + walk, + formatChangeMessage, +} from '@contractual/differs.json-schema'; + +export type { + CompareResult, + CompareOptions, + StrandsTrace, + StrandsCompatibility, + StrandsVersion, + SemanticVersion, + JsonSchemaDraft, + ResolvedSchema, + ResolveResult, +} from '@contractual/differs.json-schema'; + +// Re-export runner for custom commands +export { executeCustomCommand, parseCustomLintOutput, parseCustomDiffOutput } from './runner.js'; + +/** + * Register all built-in governance engines + * + * Call this function at CLI startup to make all engines available + * through the registry. + */ +export function registerAllEngines(): void { + // Register linters + registerLinter('openapi', lintOpenAPI); + registerLinter('json-schema', lintJsonSchema); + + // Register differs + registerDiffer('openapi', diffOpenApi); + registerDiffer('json-schema', diffJsonSchema); + + // Phase 2: + // registerLinter('asyncapi', lintAsyncAPI); + // registerLinter('odcs', lintODCS); + // registerDiffer('asyncapi', diffAsyncAPI); + // registerDiffer('odcs', diffODCS); +} + +// Auto-register on import +registerAllEngines(); diff --git a/packages/governance/linters/index.ts b/packages/governance/linters/index.ts new file mode 100644 index 0000000..42dc1a1 --- /dev/null +++ b/packages/governance/linters/index.ts @@ -0,0 +1,20 @@ +/** + * Linters - Export all linter implementations + */ + +export { lintOpenAPI } from './openapi-redocly.js'; +export { lintJsonSchema, lintJsonSchemaObject } from './json-schema-ajv.js'; +export type { JsonSchemaLintOptions } from './json-schema-ajv.js'; + +// JSON Schema lint rules (for programmatic access) +export { + runLintRules, + getAvailableRules, + getRuleDescription, + LINT_RULES, +} from './json-schema-rules.js'; +export type { LintRule, SchemaNode } from './json-schema-rules.js'; + +// Phase 2: +// export { lintAsyncAPI } from './asyncapi-parser.js'; +// export { lintODCS } from './odcs-ajv.js'; diff --git a/packages/governance/linters/json-schema-ajv.ts b/packages/governance/linters/json-schema-ajv.ts new file mode 100644 index 0000000..5c7d530 --- /dev/null +++ b/packages/governance/linters/json-schema-ajv.ts @@ -0,0 +1,202 @@ +/** + * JSON Schema Linter using ajv + custom rules + * + * Two-phase linting: + * 1. Meta-validation: Validates that the schema is itself valid JSON Schema (ajv) + * 2. Style rules: Checks for best practices, anti-patterns, and quality (custom rules) + * + * @see https://github.com/sourcemeta/jsonschema + * @see https://github.com/orgs/json-schema-org/discussions/323 + */ + +import { readFile } from 'node:fs/promises'; +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +import type { LintResult, LintIssue, LintOptions } from '@contractual/types'; +import { runLintRules } from './json-schema-rules.js'; +import type { SchemaNode } from './json-schema-rules.js'; + +const AjvConstructor = Ajv.default ?? Ajv; +const addFormatsFunc = addFormats.default ?? addFormats; + +/** + * Options for JSON Schema linting (extends base LintOptions) + */ +export interface JsonSchemaLintOptions extends LintOptions { + /** Rules to enable (if not specified, all rules are enabled) */ + enabledRules?: Set; + /** Rules to disable */ + disabledRules?: Set; + /** Skip meta-validation (ajv) */ + skipMetaValidation?: boolean; + /** Skip style rules */ + skipStyleRules?: boolean; +} + +/** + * Lint a JSON Schema using ajv meta-validation and custom style rules + * + * @param specPath - Path to the JSON Schema file + * @param options - Linting options + * @returns Lint result with errors and warnings + */ +export async function lintJsonSchema( + specPath: string, + options: JsonSchemaLintOptions = {} +): Promise { + const errors: LintIssue[] = []; + const warnings: LintIssue[] = []; + + let schema: SchemaNode; + + try { + const content = await readFile(specPath, 'utf-8'); + schema = JSON.parse(content) as SchemaNode; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error reading schema'; + + errors.push({ + path: '/', + message: `Failed to read or parse schema: ${message}`, + rule: 'parse-error', + severity: 'error', + }); + + return { contract: '', specPath, errors, warnings }; + } + + // Phase 1: Meta-validation using ajv + if (!options.skipMetaValidation) { + const ajv = new AjvConstructor({ + allErrors: true, + strict: false, + validateSchema: true, + }); + addFormatsFunc(ajv); + + const valid = ajv.validateSchema(schema); + + if (!valid && ajv.errors) { + for (const err of ajv.errors) { + errors.push({ + path: err.instancePath || '/', + message: `${err.keyword}: ${err.message}`, + rule: `meta:${err.keyword}`, + severity: 'error', + }); + } + } + } + + // Phase 2: Style rules + if (!options.skipStyleRules) { + const styleIssues = runLintRules(schema, options.enabledRules, options.disabledRules); + + // Partition into errors and warnings + for (const issue of styleIssues) { + if (issue.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } + + // Additional meta-level warnings + + // Warn about unknown $schema versions + if (schema.$schema && typeof schema.$schema === 'string') { + const schemaUri = schema.$schema; + let isKnown = false; + try { + const uri = new URL(schemaUri); + const paths = [ + '/draft-04/schema', + '/draft-06/schema', + '/draft-07/schema', + '/draft/2019-09/schema', + '/draft/2020-12/schema', + ]; + isKnown = + ['http:', 'https:'].includes(uri.protocol) && + uri.hostname === 'json-schema.org' && + paths.includes(uri.pathname); + } catch { + // Malformed identifiers are unrecognized drafts. + } + + if (!isKnown) { + warnings.push({ + path: '/$schema', + message: `Unknown schema draft: ${schemaUri}. Validation may be incomplete.`, + rule: 'unknown-draft', + severity: 'warning', + }); + } + } + + return { + contract: '', + specPath, + errors, + warnings, + }; +} + +/** + * Lint a JSON Schema object directly (no file I/O) + * + * @param schema - JSON Schema object + * @param options - Linting options + * @returns Lint result with errors and warnings + */ +export async function lintJsonSchemaObject( + schema: SchemaNode, + options: JsonSchemaLintOptions = {} +): Promise { + const errors: LintIssue[] = []; + const warnings: LintIssue[] = []; + + // Phase 1: Meta-validation using ajv + if (!options.skipMetaValidation) { + const ajv = new AjvConstructor({ + allErrors: true, + strict: false, + validateSchema: true, + }); + addFormatsFunc(ajv); + + const valid = ajv.validateSchema(schema); + + if (!valid && ajv.errors) { + for (const err of ajv.errors) { + errors.push({ + path: err.instancePath || '/', + message: `${err.keyword}: ${err.message}`, + rule: `meta:${err.keyword}`, + severity: 'error', + }); + } + } + } + + // Phase 2: Style rules + if (!options.skipStyleRules) { + const styleIssues = runLintRules(schema, options.enabledRules, options.disabledRules); + + for (const issue of styleIssues) { + if (issue.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } + + return { + contract: '', + specPath: '', + errors, + warnings, + }; +} diff --git a/packages/governance/linters/json-schema-rules.ts b/packages/governance/linters/json-schema-rules.ts new file mode 100644 index 0000000..489b027 --- /dev/null +++ b/packages/governance/linters/json-schema-rules.ts @@ -0,0 +1,835 @@ +/** + * JSON Schema Linting Rules + * + * Native TypeScript implementation of linting rules for JSON Schema. + * Based on best practices from Sourcemeta, JSON Schema org, and community discussions. + * + * @see https://github.com/sourcemeta/jsonschema + * @see https://github.com/orgs/json-schema-org/discussions/323 + */ + +import type { LintIssue, LintSeverity } from '@contractual/types'; +import { type ResolvedSchema, isSchemaObject } from '@contractual/differs.json-schema'; + +// Re-export ResolvedSchema as SchemaNode for linting API +export type { ResolvedSchema as SchemaNode } from '@contractual/differs.json-schema'; + +/** + * Lint rule definition + */ +export interface LintRule { + /** Rule identifier */ + id: string; + /** Human-readable description */ + description: string; + /** Default severity */ + severity: LintSeverity; + /** Check function - returns issues found at this schema node */ + check: (schema: ResolvedSchema, path: string, root: ResolvedSchema) => LintIssue[]; +} + +/** + * Keywords that only apply to specific types + */ +const TYPE_SPECIFIC_KEYWORDS: Record = { + string: ['minLength', 'maxLength', 'pattern', 'format'], + number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'], + integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'], + array: [ + 'items', + 'additionalItems', + 'prefixItems', + 'contains', + 'minItems', + 'maxItems', + 'uniqueItems', + 'minContains', + 'maxContains', + ], + object: [ + 'properties', + 'additionalProperties', + 'required', + 'minProperties', + 'maxProperties', + 'patternProperties', + 'propertyNames', + ], + boolean: [], + null: [], +}; + +/** + * Known JSON Schema format values + */ +const KNOWN_FORMATS = new Set([ + // Dates and times (RFC 3339) + 'date-time', + 'date', + 'time', + 'duration', + // Email (RFC 5321/5322) + 'email', + 'idn-email', + // Hostnames (RFC 1123/5891) + 'hostname', + 'idn-hostname', + // IP addresses (RFC 2673/4291) + 'ipv4', + 'ipv6', + // URIs (RFC 3986/3987) + 'uri', + 'uri-reference', + 'iri', + 'iri-reference', + 'uri-template', + // JSON Pointer (RFC 6901) + 'json-pointer', + 'relative-json-pointer', + // Regex (ECMA 262) + 'regex', + // UUID (RFC 4122) + 'uuid', +]); + +/** + * All built-in linting rules + */ +export const LINT_RULES: LintRule[] = [ + // ========================================== + // Schema Declaration Rules + // ========================================== + { + id: 'missing-schema', + description: 'Root schema should declare $schema', + severity: 'warning', + check: (schema, path, root) => { + // Only check at root + if (path !== '/' || schema !== root) return []; + if (!schema.$schema) { + return [ + { + path: '/', + message: + 'No $schema declared. Consider adding "$schema": "https://json-schema.org/draft/2020-12/schema"', + rule: 'missing-schema', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'schema-not-at-root', + description: '$schema should only appear at resource root (with $id) or document root', + severity: 'warning', + check: (schema, path, root) => { + if (path === '/' || schema === root) return []; + if (schema.$schema && !schema.$id) { + return [ + { + path, + message: + '$schema without $id in non-root location. $schema should only appear at resource roots.', + rule: 'schema-not-at-root', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + // ========================================== + // Metadata Rules + // ========================================== + { + id: 'missing-title', + description: 'Root schema should have a title', + severity: 'warning', + check: (schema, path, root) => { + if (path !== '/' || schema !== root) return []; + if (!schema.title) { + return [ + { + path: '/', + message: 'Root schema has no title. Consider adding one for documentation.', + rule: 'missing-title', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'missing-description', + description: 'Root schema should have a description', + severity: 'warning', + check: (schema, path, root) => { + if (path !== '/' || schema !== root) return []; + if (!schema.description) { + return [ + { + path: '/', + message: 'Root schema has no description. Consider adding one for documentation.', + rule: 'missing-description', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + // ========================================== + // Enum/Const Rules + // ========================================== + { + id: 'enum-to-const', + description: 'An enum with a single value should use const instead', + severity: 'warning', + check: (schema, path) => { + if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 1) { + return [ + { + path, + message: `An 'enum' with a single value can be expressed as 'const'. Use: "const": ${JSON.stringify(schema.enum[0])}`, + rule: 'enum-to-const', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'enum-with-type', + description: 'Using type with enum is redundant when enum values are all the same type', + severity: 'warning', + check: (schema, path) => { + if (!schema.enum || !schema.type) return []; + + const enumValues = schema.enum; + if (!Array.isArray(enumValues) || enumValues.length === 0) return []; + + // Determine types of all enum values + const enumTypes = new Set( + enumValues.map((v) => { + if (v === null) return 'null'; + if (Array.isArray(v)) return 'array'; + return typeof v; + }) + ); + + // Map JS types to JSON Schema types + const jsToSchema: Record = { + string: 'string', + number: 'number', + boolean: 'boolean', + object: 'object', + null: 'null', + array: 'array', + }; + + const schemaTypes = new Set([...enumTypes].map((t) => jsToSchema[t] || t)); + const declaredType = new Set( + Array.isArray(schema.type) ? schema.type : [schema.type as string] + ); + + // Check if type declaration matches enum value types exactly + const typesMatch = + schemaTypes.size === declaredType.size && + [...schemaTypes].every((t) => declaredType.has(t)); + + if (typesMatch) { + return [ + { + path, + message: "'type' is redundant when 'enum' values already constrain the type", + rule: 'enum-with-type', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'const-with-type', + description: 'Using type with const is redundant when const value determines the type', + severity: 'warning', + check: (schema, path) => { + if (schema.const === undefined || !schema.type) return []; + + const constValue = schema.const; + let constType: string; + + if (constValue === null) { + constType = 'null'; + } else if (Array.isArray(constValue)) { + constType = 'array'; + } else { + constType = typeof constValue; + } + + // Map JS type to JSON Schema type + const jsToSchema: Record = { + string: 'string', + number: 'number', + boolean: 'boolean', + object: 'object', + null: 'null', + array: 'array', + }; + const schemaConstType = jsToSchema[constType] || constType; + + const declaredType = Array.isArray(schema.type) ? schema.type : [schema.type]; + + if (declaredType.length === 1 && declaredType[0] === schemaConstType) { + return [ + { + path, + message: "'type' is redundant when 'const' value already determines the type", + rule: 'const-with-type', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + // ========================================== + // Conditional Schema Rules + // ========================================== + { + id: 'if-without-then-else', + description: 'if without then or else is unnecessary', + severity: 'warning', + check: (schema, path) => { + if (schema.if && !schema.then && !schema.else) { + return [ + { + path, + message: "'if' without 'then' or 'else' has no effect and can be removed", + rule: 'if-without-then-else', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'then-else-without-if', + description: 'then or else without if is unnecessary', + severity: 'warning', + check: (schema, path) => { + const issues: LintIssue[] = []; + if (schema.then && !schema.if) { + issues.push({ + path, + message: "'then' without 'if' has no effect and can be removed", + rule: 'then-else-without-if', + severity: 'warning', + }); + } + if (schema.else && !schema.if) { + issues.push({ + path, + message: "'else' without 'if' has no effect and can be removed", + rule: 'then-else-without-if', + severity: 'warning', + }); + } + return issues; + }, + }, + + // ========================================== + // Array Constraint Rules + // ========================================== + { + id: 'additional-items-redundant', + description: 'additionalItems is ignored when items is a schema (not tuple)', + severity: 'warning', + check: (schema, path) => { + // additionalItems only matters when items is an array (tuple validation) + // When items is a schema, additionalItems is ignored + if (schema.additionalItems !== undefined && schema.items && !Array.isArray(schema.items)) { + return [ + { + path, + message: + "'additionalItems' is ignored when 'items' is a schema (not a tuple). Remove 'additionalItems' or use tuple validation.", + rule: 'additional-items-redundant', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'contains-required', + description: 'minContains or maxContains without contains is unnecessary', + severity: 'warning', + check: (schema, path) => { + const issues: LintIssue[] = []; + if (schema.minContains !== undefined && !schema.contains) { + issues.push({ + path, + message: "'minContains' without 'contains' has no effect", + rule: 'contains-required', + severity: 'warning', + }); + } + if (schema.maxContains !== undefined && !schema.contains) { + issues.push({ + path, + message: "'maxContains' without 'contains' has no effect", + rule: 'contains-required', + severity: 'warning', + }); + } + return issues; + }, + }, + + // ========================================== + // Type Compatibility Rules + // ========================================== + { + id: 'type-incompatible-keywords', + description: 'Validation keywords that do not apply to the declared type', + severity: 'warning', + check: (schema, path) => { + if (!schema.type || Array.isArray(schema.type)) return []; + + const declaredType = schema.type as string; + const applicableKeywords = TYPE_SPECIFIC_KEYWORDS[declaredType] || []; + const issues: LintIssue[] = []; + + // Check for keywords that apply to other types + for (const [type, keywords] of Object.entries(TYPE_SPECIFIC_KEYWORDS)) { + if (type === declaredType) continue; + + for (const keyword of keywords) { + if (schema[keyword] !== undefined && !applicableKeywords.includes(keyword)) { + issues.push({ + path, + message: `'${keyword}' applies to type '${type}' but schema declares type '${declaredType}'`, + rule: 'type-incompatible-keywords', + severity: 'warning', + }); + } + } + } + + return issues; + }, + }, + + // ========================================== + // Range Validation Rules + // ========================================== + { + id: 'invalid-numeric-range', + description: 'maximum should be greater than or equal to minimum', + severity: 'error', + check: (schema, path) => { + const issues: LintIssue[] = []; + + if (typeof schema.minimum === 'number' && typeof schema.maximum === 'number') { + if (schema.maximum < schema.minimum) { + issues.push({ + path, + message: `'maximum' (${schema.maximum}) is less than 'minimum' (${schema.minimum})`, + rule: 'invalid-numeric-range', + severity: 'error', + }); + } + } + + // Handle exclusive bounds (draft-06+ where they are numbers) + if ( + typeof schema.exclusiveMinimum === 'number' && + typeof schema.exclusiveMaximum === 'number' + ) { + if (schema.exclusiveMaximum <= schema.exclusiveMinimum) { + issues.push({ + path, + message: `'exclusiveMaximum' (${schema.exclusiveMaximum}) is not greater than 'exclusiveMinimum' (${schema.exclusiveMinimum})`, + rule: 'invalid-numeric-range', + severity: 'error', + }); + } + } + + return issues; + }, + }, + + { + id: 'invalid-length-range', + description: 'maxLength should be greater than or equal to minLength', + severity: 'error', + check: (schema, path) => { + if (typeof schema.minLength === 'number' && typeof schema.maxLength === 'number') { + if (schema.maxLength < schema.minLength) { + return [ + { + path, + message: `'maxLength' (${schema.maxLength}) is less than 'minLength' (${schema.minLength})`, + rule: 'invalid-length-range', + severity: 'error', + }, + ]; + } + } + return []; + }, + }, + + { + id: 'invalid-items-range', + description: 'maxItems should be greater than or equal to minItems', + severity: 'error', + check: (schema, path) => { + if (typeof schema.minItems === 'number' && typeof schema.maxItems === 'number') { + if (schema.maxItems < schema.minItems) { + return [ + { + path, + message: `'maxItems' (${schema.maxItems}) is less than 'minItems' (${schema.minItems})`, + rule: 'invalid-items-range', + severity: 'error', + }, + ]; + } + } + return []; + }, + }, + + { + id: 'invalid-properties-range', + description: 'maxProperties should be greater than or equal to minProperties', + severity: 'error', + check: (schema, path) => { + if (typeof schema.minProperties === 'number' && typeof schema.maxProperties === 'number') { + if (schema.maxProperties < schema.minProperties) { + return [ + { + path, + message: `'maxProperties' (${schema.maxProperties}) is less than 'minProperties' (${schema.minProperties})`, + rule: 'invalid-properties-range', + severity: 'error', + }, + ]; + } + } + return []; + }, + }, + + // ========================================== + // Format Rules + // ========================================== + { + id: 'unknown-format', + description: 'Unknown format value may not be validated', + severity: 'warning', + check: (schema, path) => { + if (typeof schema.format === 'string' && !KNOWN_FORMATS.has(schema.format)) { + return [ + { + path, + message: `Unknown format '${schema.format}'. Custom formats may not be validated by all implementations.`, + rule: 'unknown-format', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + // ========================================== + // Empty Schema Rules + // ========================================== + { + id: 'empty-enum', + description: 'Empty enum matches nothing and is likely an error', + severity: 'error', + check: (schema, path) => { + if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 0) { + return [ + { + path, + message: "Empty 'enum' array will never validate any value", + rule: 'empty-enum', + severity: 'error', + }, + ]; + } + return []; + }, + }, + + { + id: 'empty-required', + description: 'Empty required array is unnecessary', + severity: 'warning', + check: (schema, path) => { + if (schema.required && Array.isArray(schema.required) && schema.required.length === 0) { + return [ + { + path, + message: "Empty 'required' array can be removed", + rule: 'empty-required', + severity: 'warning', + }, + ]; + } + return []; + }, + }, + + { + id: 'empty-allof-anyof-oneof', + description: 'Empty allOf/anyOf/oneOf is likely an error', + severity: 'error', + check: (schema, path) => { + const issues: LintIssue[] = []; + for (const keyword of ['allOf', 'anyOf', 'oneOf'] as const) { + const value = schema[keyword]; + if (value && Array.isArray(value) && value.length === 0) { + issues.push({ + path, + message: `Empty '${keyword}' array ${keyword === 'anyOf' || keyword === 'oneOf' ? 'will never validate' : 'is redundant'}`, + rule: 'empty-allof-anyof-oneof', + severity: keyword === 'allOf' ? 'warning' : 'error', + }); + } + } + return issues; + }, + }, + + // ========================================== + // Required Properties Rules + // ========================================== + { + id: 'required-undefined-property', + description: 'Required property is not defined in properties', + severity: 'warning', + check: (schema, path) => { + if (!schema.required || !schema.properties) return []; + + const issues: LintIssue[] = []; + const definedProps = new Set(Object.keys(schema.properties)); + + for (const requiredProp of schema.required) { + if (!definedProps.has(requiredProp)) { + issues.push({ + path, + message: `Required property '${requiredProp}' is not defined in 'properties'`, + rule: 'required-undefined-property', + severity: 'warning', + }); + } + } + return issues; + }, + }, + + { + id: 'duplicate-required', + description: 'Duplicate entries in required array', + severity: 'warning', + check: (schema, path) => { + if (!schema.required || !Array.isArray(schema.required)) return []; + + const seen = new Set(); + const duplicates = new Set(); + + for (const prop of schema.required) { + if (seen.has(prop)) { + duplicates.add(prop); + } + seen.add(prop); + } + + if (duplicates.size > 0) { + return [ + { + path, + message: `Duplicate entries in 'required': ${[...duplicates].join(', ')}`, + rule: 'duplicate-required', + severity: 'warning', + }, + ]; + } + return []; + }, + }, +]; + +/** + * Run all lint rules against a schema, recursively walking all subschemas + */ +export function runLintRules( + schema: ResolvedSchema, + enabledRules?: Set, + disabledRules?: Set +): LintIssue[] { + const issues: LintIssue[] = []; + const activeRules = LINT_RULES.filter((rule) => { + if (disabledRules?.has(rule.id)) return false; + if (enabledRules && !enabledRules.has(rule.id)) return false; + return true; + }); + + walkSchema(schema, '/', schema, (node, path, root) => { + for (const rule of activeRules) { + const ruleIssues = rule.check(node, path, root); + issues.push(...ruleIssues); + } + }); + + return issues; +} + +/** + * Walk all subschemas in a JSON Schema document + */ +function walkSchema( + schema: ResolvedSchema, + path: string, + root: ResolvedSchema, + visitor: (node: ResolvedSchema, path: string, root: ResolvedSchema) => void +): void { + if (!isSchemaObject(schema)) return; + + visitor(schema, path, root); + + // Properties + if (schema.properties) { + for (const [key, value] of Object.entries(schema.properties)) { + if (isSchemaObject(value)) { + walkSchema(value, `${path}/properties/${key}`, root, visitor); + } + } + } + + // Additional properties + if (isSchemaObject(schema.additionalProperties)) { + walkSchema(schema.additionalProperties, `${path}/additionalProperties`, root, visitor); + } + + // Pattern properties + if (schema.patternProperties) { + for (const [pattern, value] of Object.entries(schema.patternProperties)) { + if (isSchemaObject(value)) { + walkSchema( + value, + `${path}/patternProperties/${encodeURIComponent(pattern)}`, + root, + visitor + ); + } + } + } + + // Property names + if (isSchemaObject(schema.propertyNames)) { + walkSchema(schema.propertyNames, `${path}/propertyNames`, root, visitor); + } + + // Items (array or single schema) + if (schema.items) { + if (Array.isArray(schema.items)) { + schema.items.forEach((item, i) => { + if (isSchemaObject(item)) { + walkSchema(item, `${path}/items/${i}`, root, visitor); + } + }); + } else if (isSchemaObject(schema.items)) { + walkSchema(schema.items, `${path}/items`, root, visitor); + } + } + + // Prefix items (2020-12) + if (schema.prefixItems && Array.isArray(schema.prefixItems)) { + schema.prefixItems.forEach((item, i) => { + if (isSchemaObject(item)) { + walkSchema(item, `${path}/prefixItems/${i}`, root, visitor); + } + }); + } + + // Contains + if (isSchemaObject(schema.contains)) { + walkSchema(schema.contains, `${path}/contains`, root, visitor); + } + + // Conditional + if (isSchemaObject(schema.if)) { + walkSchema(schema.if, `${path}/if`, root, visitor); + } + if (isSchemaObject(schema.then)) { + walkSchema(schema.then, `${path}/then`, root, visitor); + } + if (isSchemaObject(schema.else)) { + walkSchema(schema.else, `${path}/else`, root, visitor); + } + + // Composition + for (const keyword of ['allOf', 'anyOf', 'oneOf'] as const) { + const arr = schema[keyword]; + if (arr && Array.isArray(arr)) { + arr.forEach((item, i) => { + if (isSchemaObject(item)) { + walkSchema(item, `${path}/${keyword}/${i}`, root, visitor); + } + }); + } + } + + // Not + if (isSchemaObject(schema.not)) { + walkSchema(schema.not, `${path}/not`, root, visitor); + } + + // Definitions ($defs) + if (schema.$defs) { + for (const [key, value] of Object.entries(schema.$defs)) { + if (isSchemaObject(value)) { + walkSchema(value, `${path}/$defs/${key}`, root, visitor); + } + } + } +} + +/** + * Get all available rule IDs + */ +export function getAvailableRules(): string[] { + return LINT_RULES.map((r) => r.id); +} + +/** + * Get rule description by ID + */ +export function getRuleDescription(ruleId: string): string | undefined { + return LINT_RULES.find((r) => r.id === ruleId)?.description; +} diff --git a/packages/governance/linters/openapi-redocly.ts b/packages/governance/linters/openapi-redocly.ts new file mode 100644 index 0000000..fc6862e --- /dev/null +++ b/packages/governance/linters/openapi-redocly.ts @@ -0,0 +1,78 @@ +/** + * OpenAPI Linter using @redocly/openapi-core + * + * Wraps Redocly's OpenAPI linting library to validate OpenAPI specs. + * Users can place a .redocly.yaml in their repo for custom rules. + */ + +import type { LintResult, LintIssue } from '@contractual/types'; + +/** + * Lint an OpenAPI specification using Redocly + * + * @param specPath - Path to the OpenAPI spec file + * @returns Lint result with errors and warnings + */ +export async function lintOpenAPI(specPath: string): Promise { + // Dynamic import to handle the library being optional + let lint: typeof import('@redocly/openapi-core').lint; + let loadConfig: typeof import('@redocly/openapi-core').loadConfig; + + try { + const redocly = await import('@redocly/openapi-core'); + lint = redocly.lint; + loadConfig = redocly.loadConfig; + } catch { + throw new Error( + 'OpenAPI linting requires @redocly/openapi-core. ' + + 'Install it with: npm install @redocly/openapi-core' + ); + } + + const errors: LintIssue[] = []; + const warnings: LintIssue[] = []; + + try { + // Load config from .redocly.yaml if present, or use defaults + const config = await loadConfig(); + + // Run the linter + const results = await lint({ + ref: specPath, + config, + }); + + // Process problems + for (const problem of results) { + const issue: LintIssue = { + path: problem.location?.[0]?.pointer ?? '', + message: problem.message, + rule: problem.ruleId, + severity: problem.severity === 'error' ? 'error' : 'warning', + }; + + if (issue.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } catch (error: unknown) { + // Handle parse errors or file not found + const message = error instanceof Error ? error.message : 'Unknown error during linting'; + + errors.push({ + path: '/', + message, + rule: 'parse-error', + severity: 'error', + }); + } + + return { + contract: '', // Filled by caller + specPath, + errors, + warnings, + }; +} diff --git a/packages/governance/linters/schema-uri.test.ts b/packages/governance/linters/schema-uri.test.ts new file mode 100644 index 0000000..a622d3f --- /dev/null +++ b/packages/governance/linters/schema-uri.test.ts @@ -0,0 +1,24 @@ +import { afterEach, expect, test } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { lintJsonSchema } from './json-schema-ajv.js'; + +const directories: string[] = []; +afterEach(() => + directories.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true })) +); +test.each([ + ['https://json-schema.org/draft-07/schema#', false], + ['https://json-schema.org.evil.test/draft-07/schema', true], + ['https://evil.test/json-schema.org/draft-07/schema', true], + ['https://evil.test/?draft-07=json-schema.org', true], + ['not-a-url', true], +])('checks the parsed schema URI: %s', async ($schema, unknown) => { + const directory = mkdtempSync(join(tmpdir(), 'schema-uri-test-')); + directories.push(directory); + const path = join(directory, 'schema.json'); + writeFileSync(path, JSON.stringify({ $schema, type: 'object' })); + const result = await lintJsonSchema(path, { skipMetaValidation: true, skipStyleRules: true }); + expect(result.warnings.some((issue) => issue.rule === 'unknown-draft')).toBe(unknown); +}); diff --git a/packages/governance/package.json b/packages/governance/package.json new file mode 100644 index 0000000..9f49f75 --- /dev/null +++ b/packages/governance/package.json @@ -0,0 +1,60 @@ +{ + "name": "@contractual/governance", + "private": false, + "version": "0.1.0-beta.0", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./linters": { + "types": "./dist/linters/index.d.ts", + "import": "./dist/linters/index.js" + }, + "./differs": { + "types": "./dist/differs/index.d.ts", + "import": "./dist/differs/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/governance" + }, + "homepage": "https://contractual.dev", + "bugs": { + "url": "https://github.com/codotech/contractual/issues" + }, + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "pnpm rimraf dist", + "build": "tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch", + "lint": "eslint \"linters/**/*.ts\" \"differs/**/*.ts\" \"*.ts\"" + }, + "files": [ + "dist", + "README.md" + ], + "dependencies": { + "@contractual/differs.json-schema": "workspace:*", + "@contractual/differs.openapi": "workspace:*", + "@contractual/types": "workspace:*", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + }, + "devDependencies": { + "@redocly/openapi-core": "^1.34.20", + "vitest": "^4.1.11" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/governance/registry.ts b/packages/governance/registry.ts new file mode 100644 index 0000000..2ed1f78 --- /dev/null +++ b/packages/governance/registry.ts @@ -0,0 +1,198 @@ +/** + * Governance Registry + * + * Central registry for linters and differs. CLI Core uses this to resolve + * the correct engine for each contract type. + */ + +import type { LintFn, DiffFn, ContractType } from '@contractual/types'; + +const linters = new Map(); +const differs = new Map(); + +/** + * Register a linter for a contract type + */ +export function registerLinter(type: ContractType, linter: LintFn): void { + linters.set(type, linter); +} + +/** + * Register a differ for a contract type + */ +export function registerDiffer(type: ContractType, differ: DiffFn): void { + differs.set(type, differ); +} + +/** + * Get the linter for a contract type + * + * @param type - Contract type (openapi, json-schema, etc.) + * @param override - Optional override: built-in name or custom command + * @returns Linter function or null if not found + */ +export function getLinter(type: ContractType, override?: string | false): LintFn | null { + // If override is explicitly false, disable linting + if (override === false) { + return null; + } + + // If override is a custom command (contains {spec}), create custom executor + if (override && override.includes('{spec}')) { + return createCustomLinter(override); + } + + // If override is a known built-in name, resolve it + if (override) { + const builtIn = resolveBuiltInLinter(override); + if (builtIn) return builtIn; + } + + // Return default for type + return linters.get(type) ?? null; +} + +/** + * Get the differ for a contract type + * + * @param type - Contract type (openapi, json-schema, etc.) + * @param override - Optional override: built-in name or custom command + * @returns Differ function or null if not found + */ +export function getDiffer(type: ContractType, override?: string | false): DiffFn | null { + // If override is explicitly false, disable diffing + if (override === false) { + return null; + } + + // If override is a custom command (contains {old} or {new}), create custom executor + if (override && (override.includes('{old}') || override.includes('{new}'))) { + return createCustomDiffer(override); + } + + // If override is a known built-in name, resolve it + if (override) { + const builtIn = resolveBuiltInDiffer(override); + if (builtIn) return builtIn; + } + + // Return default for type + return differs.get(type) ?? null; +} + +/** + * Valid contract type values for type checking + */ +const VALID_CONTRACT_TYPES: readonly ContractType[] = [ + 'openapi', + 'json-schema', + 'asyncapi', + 'odcs', +] as const; + +/** + * Type guard for ContractType + */ +function isContractType(value: string): value is ContractType { + return VALID_CONTRACT_TYPES.includes(value as ContractType); +} + +/** + * Resolve a built-in linter by name + */ +function resolveBuiltInLinter(name: string): LintFn | null { + // Check if name is a valid contract type + if (isContractType(name)) { + const linter = linters.get(name); + if (linter) return linter; + } + + // Special named overrides + switch (name.toLowerCase()) { + case 'redocly': + case 'openapi-redocly': + return linters.get('openapi') ?? null; + case 'spectral': + // Spectral is an alternative OpenAPI linter - could add support later + return linters.get('openapi') ?? null; + case 'ajv': + case 'json-schema-ajv': + return linters.get('json-schema') ?? null; + default: + return null; + } +} + +/** + * Resolve a built-in differ by name + */ +function resolveBuiltInDiffer(name: string): DiffFn | null { + // Check if name is a valid contract type + if (isContractType(name)) { + const differ = differs.get(name); + if (differ) return differ; + } + + // Special named overrides + switch (name.toLowerCase()) { + case 'oasdiff': + case 'openapi-oasdiff': + return differs.get('openapi') ?? null; + case 'json-schema-differ': + return differs.get('json-schema') ?? null; + default: + return null; + } +} + +/** + * Create a custom linter from a command template + */ +function createCustomLinter(commandTemplate: string): LintFn { + return async (specPath: string) => { + const { executeCustomCommand, parseCustomLintOutput } = await import('./runner.js'); + const command = commandTemplate.replace('{spec}', specPath); + const output = await executeCustomCommand(command); + return parseCustomLintOutput(output); + }; +} + +/** + * Create a custom differ from a command template + */ +function createCustomDiffer(commandTemplate: string): DiffFn { + return async (oldSpecPath: string, newSpecPath: string) => { + const { executeCustomCommand, parseCustomDiffOutput } = await import('./runner.js'); + const command = commandTemplate.replace('{old}', oldSpecPath).replace('{new}', newSpecPath); + const output = await executeCustomCommand(command); + return parseCustomDiffOutput(output); + }; +} + +/** + * Check if a linter is registered for a contract type + */ +export function hasLinter(type: ContractType): boolean { + return linters.has(type); +} + +/** + * Check if a differ is registered for a contract type + */ +export function hasDiffer(type: ContractType): boolean { + return differs.has(type); +} + +/** + * Get all registered contract types for linting + */ +export function getRegisteredLinterTypes(): ContractType[] { + return Array.from(linters.keys()); +} + +/** + * Get all registered contract types for diffing + */ +export function getRegisteredDifferTypes(): ContractType[] { + return Array.from(differs.keys()); +} diff --git a/packages/governance/runner.ts b/packages/governance/runner.ts new file mode 100644 index 0000000..0812b07 --- /dev/null +++ b/packages/governance/runner.ts @@ -0,0 +1,366 @@ +/** + * Custom Command Runner + * + * Executes user-defined governance commands and parses their output. + */ + +import { execSync } from 'node:child_process'; +import type { LintResult, DiffResult, LintIssue, Change } from '@contractual/types'; + +/** + * Shape of exec error with additional properties + */ +interface ExecError extends Error { + stdout?: string; + stderr?: string; + status?: number; + signal?: string; +} + +/** + * Type guard for ExecError + */ +function isExecError(error: unknown): error is ExecError { + return error instanceof Error; +} + +/** + * Execute a custom command and return its output + * + * Note: This function is async for API consistency with other governance functions, + * even though the underlying execSync is synchronous. This allows for future + * migration to async child_process.spawn if needed. + * + * @param command - Command to execute + * @param timeout - Timeout in milliseconds (default: 60000) + * @returns Command stdout + * @throws Error if command fails + */ +export async function executeCustomCommand(command: string, timeout = 60000): Promise { + try { + const stdout = execSync(command, { + encoding: 'utf-8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + }); + return stdout; + } catch (error: unknown) { + // Type-safe error handling + if (isExecError(error)) { + // Some tools exit with non-zero when issues found - return stdout if available + if (error.stdout) { + return error.stdout; + } + + throw new Error( + `Custom command failed: ${command}\n` + + `Exit code: ${error.status ?? 'unknown'}\n` + + `Stderr: ${error.stderr ?? error.message}` + ); + } + + throw new Error(`Custom command failed: ${command}\n${String(error)}`); + } +} + +/** + * Parse custom lint command output into LintResult + * + * Attempts to parse as JSON first, then falls back to line-based parsing. + */ +export function parseCustomLintOutput(output: string): LintResult { + const trimmed = output.trim(); + + // Try JSON parsing first + if (trimmed.startsWith('[') || trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed); + return normalizeJsonLintOutput(parsed); + } catch { + // Fall through to line-based parsing + } + } + + // Line-based parsing for text output + return parseLineLintOutput(trimmed); +} + +/** + * Parse custom diff command output into DiffResult + * + * Attempts to parse as JSON first, then falls back to line-based parsing. + */ +export function parseCustomDiffOutput(output: string): DiffResult { + const trimmed = output.trim(); + + // Try JSON parsing first + if (trimmed.startsWith('[') || trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed); + return normalizeJsonDiffOutput(parsed); + } catch { + // Fall through to line-based parsing + } + } + + // Line-based parsing for text output + return parseLineDiffOutput(trimmed); +} + +/** + * Normalize JSON lint output from various tools + */ +function normalizeJsonLintOutput(parsed: unknown): LintResult { + const errors: LintIssue[] = []; + const warnings: LintIssue[] = []; + + if (Array.isArray(parsed)) { + for (const item of parsed) { + const issue = normalizeIssue(item); + if (issue.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } else if (typeof parsed === 'object' && parsed !== null) { + // Handle object with errors/warnings arrays + const obj = parsed as Record; + if (Array.isArray(obj.errors)) { + errors.push(...obj.errors.map((e) => normalizeIssue(e, 'error'))); + } + if (Array.isArray(obj.warnings)) { + warnings.push(...obj.warnings.map((w) => normalizeIssue(w, 'warning'))); + } + if (Array.isArray(obj.problems)) { + for (const p of obj.problems) { + const issue = normalizeIssue(p); + if (issue.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } + } + + return { contract: '', specPath: '', errors, warnings }; +} + +/** + * Normalize a single issue from various formats + */ +function normalizeIssue(item: unknown, defaultSeverity: 'error' | 'warning' = 'error'): LintIssue { + if (typeof item === 'string') { + return { + path: '', + message: item, + severity: defaultSeverity, + }; + } + + if (typeof item !== 'object' || item === null) { + return { + path: '', + message: String(item), + severity: defaultSeverity, + }; + } + + const obj = item as Record; + + // Common field mappings + const path = String(obj.path ?? obj.location ?? obj.pointer ?? obj.instancePath ?? ''); + const message = String(obj.message ?? obj.text ?? obj.description ?? obj.msg ?? ''); + const rule = obj.rule ?? obj.ruleId ?? obj.code ?? obj.id; + const severity = normalizeSeverity(obj.severity ?? obj.level ?? defaultSeverity); + + return { + path, + message, + rule: rule ? String(rule) : undefined, + severity, + }; +} + +/** + * Normalize severity from various formats + */ +function normalizeSeverity(value: unknown): 'error' | 'warning' { + if (typeof value === 'number') { + // Common: 0=error, 1=warning, 2=info (Spectral) + // Or: 1=error, 2=warning, 3=info (some tools) + return value <= 1 ? 'error' : 'warning'; + } + if (typeof value === 'string') { + const lower = value.toLowerCase(); + if (lower === 'error' || lower === 'err' || lower === 'fatal') { + return 'error'; + } + } + return 'warning'; +} + +/** + * Parse line-based lint output + */ +function parseLineLintOutput(output: string): LintResult { + const errors: LintIssue[] = []; + const warnings: LintIssue[] = []; + + const lines = output.split('\n').filter((line) => line.trim()); + + for (const line of lines) { + // Common patterns: + // "error: message" or "warning: message" + // "path:line:col: error message" + // "[ERROR] message" + + const lowerLine = line.toLowerCase(); + const isError = + lowerLine.includes('error') || lowerLine.includes('[err]') || lowerLine.startsWith('e '); + const isWarning = + lowerLine.includes('warning') || lowerLine.includes('[warn]') || lowerLine.startsWith('w '); + + if (isError || isWarning || line.includes(':')) { + const issue: LintIssue = { + path: '', + message: line.trim(), + severity: isError ? 'error' : 'warning', + }; + + if (isError) { + errors.push(issue); + } else { + warnings.push(issue); + } + } + } + + return { contract: '', specPath: '', errors, warnings }; +} + +/** + * Normalize JSON diff output from various tools + */ +function normalizeJsonDiffOutput(parsed: unknown): DiffResult { + const changes: Change[] = []; + + if (Array.isArray(parsed)) { + for (const item of parsed) { + changes.push(normalizeChange(item)); + } + } else if (typeof parsed === 'object' && parsed !== null) { + const obj = parsed as Record; + // Handle object with changes/items array + const items = obj.changes ?? obj.items ?? obj.differences ?? []; + if (Array.isArray(items)) { + for (const item of items) { + changes.push(normalizeChange(item)); + } + } + } + + const summary = { + breaking: changes.filter((c) => c.severity === 'breaking').length, + nonBreaking: changes.filter((c) => c.severity === 'non-breaking').length, + patch: changes.filter((c) => c.severity === 'patch').length, + unknown: changes.filter((c) => c.severity === 'unknown').length, + }; + + const suggestedBump = + summary.breaking > 0 + ? 'major' + : summary.nonBreaking > 0 + ? 'minor' + : summary.patch > 0 + ? 'patch' + : 'none'; + + return { contract: '', changes, summary, suggestedBump }; +} + +/** + * Normalize a single change from various formats + */ +function normalizeChange(item: unknown): Change { + if (typeof item !== 'object' || item === null) { + return { + path: '', + severity: 'unknown', + category: 'unknown-change', + message: String(item), + }; + } + + const obj = item as Record; + + const path = String(obj.path ?? obj.location ?? obj.pointer ?? ''); + const message = String(obj.message ?? obj.text ?? obj.description ?? ''); + const category = String(obj.category ?? obj.code ?? obj.id ?? 'unknown-change'); + + // Determine severity + let severity: Change['severity'] = 'unknown'; + if (obj.severity) { + const sev = String(obj.severity).toLowerCase(); + if (sev === 'breaking' || sev === 'major') { + severity = 'breaking'; + } else if (sev === 'non-breaking' || sev === 'minor') { + severity = 'non-breaking'; + } else if (sev === 'patch') { + severity = 'patch'; + } + } else if (obj.level !== undefined) { + // oasdiff style: level 3 = breaking + const level = Number(obj.level); + if (level === 3) severity = 'breaking'; + else if (level === 2) severity = 'non-breaking'; + else if (level === 1) severity = 'patch'; + } else if (obj.breaking === true) { + severity = 'breaking'; + } + + return { + path, + severity, + category, + message, + oldValue: obj.oldValue ?? obj.old ?? obj.from, + newValue: obj.newValue ?? obj.new ?? obj.to, + }; +} + +/** + * Parse line-based diff output + */ +function parseLineDiffOutput(output: string): DiffResult { + const changes: Change[] = []; + + const lines = output.split('\n').filter((line) => line.trim()); + + for (const line of lines) { + const lowerLine = line.toLowerCase(); + const isBreaking = + lowerLine.includes('breaking') || + lowerLine.includes('removed') || + lowerLine.includes('major'); + + changes.push({ + path: '', + severity: isBreaking ? 'breaking' : 'unknown', + category: 'unknown-change', + message: line.trim(), + }); + } + + const summary = { + breaking: changes.filter((c) => c.severity === 'breaking').length, + nonBreaking: 0, + patch: 0, + unknown: changes.filter((c) => c.severity === 'unknown').length, + }; + + const suggestedBump = summary.breaking > 0 ? 'major' : 'none'; + + return { contract: '', changes, summary, suggestedBump }; +} diff --git a/packages/governance/tsconfig.build.json b/packages/governance/tsconfig.build.json new file mode 100644 index 0000000..8ed7148 --- /dev/null +++ b/packages/governance/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true + }, + "references": [ + { "path": "../types/tsconfig.build.json" } + ], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/governance/tsconfig.json b/packages/governance/tsconfig.json new file mode 100644 index 0000000..3bdc553 --- /dev/null +++ b/packages/governance/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md new file mode 100644 index 0000000..da0ac90 --- /dev/null +++ b/packages/types/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-beta.0](https://github.com/codotech/contractual/compare/@contractual/types@0.1.0-dev.5...@contractual/types@0.1.0-beta.0) (2026-10-08) + + +### Bug Fixes + +* prepare next workflow and release safeguards ([#17](https://github.com/codotech/contractual/issues/17)) ([361ad40](https://github.com/codotech/contractual/commit/361ad40678902076cbd85156985779cf217249d0)) diff --git a/packages/types/config.ts b/packages/types/config.ts new file mode 100644 index 0000000..7edb48d --- /dev/null +++ b/packages/types/config.ts @@ -0,0 +1,164 @@ +/** + * Supported contract types in Contractual. + * + * @remarks + * - `openapi` - OpenAPI 3.x specifications + * - `json-schema` - JSON Schema (Draft-07, 2019-09, 2020-12) + * - `asyncapi` - AsyncAPI 2.x/3.x specifications + * - `odcs` - Open Data Contract Standard v3.x + */ +export type ContractType = 'openapi' | 'json-schema' | 'asyncapi' | 'odcs'; + +/** + * All valid contract type values as a readonly array. + * Useful for runtime validation and iteration. + */ +export const CONTRACT_TYPES = [ + 'openapi', + 'json-schema', + 'asyncapi', + 'odcs', +] as const satisfies readonly ContractType[]; + +/** + * Definition of a single contract in the configuration file. + * + * @example + * ```yaml + * contracts: + * - name: orders-api + * type: openapi + * path: ./specs/orders.openapi.yaml + * ``` + */ +export interface ContractDefinition { + /** Unique identifier for the contract, used in changeset files and version history */ + name: string; + /** Type of schema format */ + type: ContractType; + /** Path to spec file, relative to config (can be glob pattern) */ + path: string; + /** Override linter: tool name, custom command with {spec} placeholder, or false to disable */ + lint?: string | false; + /** Override differ: tool name, custom command with {old} {new} placeholders, or false to disable */ + breaking?: string | false; + /** Sync version field inside the spec file on version bump (default: true). Set to false to skip. */ + syncVersion?: boolean; + /** Output generation commands (Phase 2) */ + generate?: string[]; +} + +/** + * Changeset behavior configuration. + */ +export interface ChangesetConfig { + /** Auto-detect change classifications from structural diff (default: true) */ + autoDetect?: boolean; + /** Require changeset for spec changes in PRs (default: true) */ + requireOnPR?: boolean; +} + +/** + * AI provider types supported by Contractual. + */ +export type AIProvider = 'anthropic'; + +/** + * AI feature toggles. + */ +export interface AIFeatures { + /** Enable PR change explanations */ + explain?: boolean; + /** Enable AI-enriched changelog entries */ + changelog?: boolean; + /** Enable spec metadata enhancement (auto-fill descriptions, examples) */ + enhance?: boolean; +} + +/** + * AI/LLM integration configuration. + * + * @remarks + * All AI features gracefully degrade when no API key is provided. + */ +export interface AIConfig { + /** AI provider (only 'anthropic' supported currently) */ + provider?: AIProvider; + /** Model identifier to use */ + model?: string; + /** Feature toggles */ + features?: AIFeatures; +} + +/** + * Versioning mode for contracts. + * + * - `independent` - Each contract has its own version (like Lerna independent mode) + * - `fixed` - All contracts share the same version + */ +export type VersioningMode = 'independent' | 'fixed'; + +/** + * Versioning configuration. + */ +export interface VersioningConfig { + /** Versioning mode (default: 'independent') */ + mode: VersioningMode; +} + +/** + * Root configuration for contractual.yaml. + * + * @example + * ```yaml + * contracts: + * - name: orders-api + * type: openapi + * path: ./specs/orders.openapi.yaml + * + * ai: + * features: + * explain: true + * changelog: true + * ``` + */ +export interface ContractualConfig { + /** List of contract definitions */ + contracts: ContractDefinition[]; + /** Versioning configuration */ + versioning?: VersioningConfig; + /** Changeset behavior configuration */ + changeset?: ChangesetConfig; + /** AI/LLM integration configuration */ + ai?: AIConfig; +} + +/** + * Resolved contract with absolute paths and validated data. + */ +export interface ResolvedContract extends ContractDefinition { + /** Absolute path to the spec file */ + absolutePath: string; +} + +/** + * Fully resolved configuration with absolute paths. + */ +export interface ResolvedConfig extends Omit { + /** Resolved contracts with absolute paths */ + contracts: ResolvedContract[]; + /** Directory containing contractual.yaml */ + configDir: string; + /** Absolute path to contractual.yaml */ + configPath: string; +} + +/** + * Type guard to check if a string is a valid ContractType. + * + * @param value - The value to check + * @returns True if the value is a valid ContractType + */ +export function isContractType(value: unknown): value is ContractType { + return typeof value === 'string' && CONTRACT_TYPES.includes(value as ContractType); +} diff --git a/packages/types/governance.ts b/packages/types/governance.ts new file mode 100644 index 0000000..a6c4db9 --- /dev/null +++ b/packages/types/governance.ts @@ -0,0 +1,374 @@ +/** + * Severity classification for detected changes between spec versions. + * + * @remarks + * - `breaking` - Changes that will break existing consumers (major bump) + * - `non-breaking` - Additive changes that are backwards compatible (minor bump) + * - `patch` - Metadata-only changes (patch bump) + * - `unknown` - Complex changes requiring manual review + */ +export type ChangeSeverity = 'breaking' | 'non-breaking' | 'patch' | 'unknown'; + +/** + * A single change detected between two spec versions. + * + * @example + * ```typescript + * const change: Change = { + * path: '/properties/amount/type', + * severity: 'breaking', + * category: 'type-changed', + * message: "Changed type of field 'amount': string -> number", + * oldValue: 'string', + * newValue: 'number' + * }; + * ``` + */ +export interface Change { + /** JSON Pointer path to the changed element (e.g., "/properties/amount/type") */ + path: string; + /** Severity classification determining version bump */ + severity: ChangeSeverity; + /** Category key for grouping similar changes (e.g., "field-removed", "type-changed") */ + category: string; + /** Human-readable description of the change */ + message: string; + /** Value in the old spec (undefined if added) */ + oldValue?: unknown; + /** Value in the new spec (undefined if removed) */ + newValue?: unknown; +} + +/** + * Summary counts of changes by severity. + */ +export interface DiffSummary { + /** Number of breaking changes detected */ + breaking: number; + /** Number of non-breaking (additive) changes detected */ + nonBreaking: number; + /** Number of patch-level (metadata) changes detected */ + patch: number; + /** Number of unclassifiable changes requiring manual review */ + unknown: number; +} + +/** + * Suggested semver bump based on detected changes. + * + * @remarks + * - `major` - Breaking changes detected + * - `minor` - Non-breaking structural changes + * - `patch` - Metadata-only changes + * - `none` - No changes detected + */ +export type SuggestedBump = 'major' | 'minor' | 'patch' | 'none'; + +/** + * Result of comparing two spec versions. + */ +export interface DiffResult { + /** Contract name from config */ + contract: string; + /** List of all detected changes */ + changes: Change[]; + /** Aggregated summary counts by severity */ + summary: DiffSummary; + /** Recommended semver bump based on highest severity change */ + suggestedBump: SuggestedBump; +} + +/** + * Severity of a lint diagnostic. + */ +export type LintSeverity = 'error' | 'warning'; + +/** + * A single lint issue found in a spec. + */ +export interface LintIssue { + /** Location in spec (JSON Pointer path or line:col) */ + path: string; + /** Human-readable issue description */ + message: string; + /** Rule identifier if available (e.g., "operation-operationId") */ + rule?: string; + /** Issue severity */ + severity: LintSeverity; +} + +/** + * Result of linting a spec file. + */ +export interface LintResult { + /** Contract name from config */ + contract: string; + /** Absolute path to the spec file that was linted */ + specPath: string; + /** List of error-level issues (cause lint failure) */ + errors: LintIssue[]; + /** List of warning-level issues (informational) */ + warnings: LintIssue[]; +} + +/** + * Options for linter functions. + */ +export interface LintOptions { + /** Custom ruleset configuration */ + ruleset?: string; + /** Additional tool-specific options */ + [key: string]: unknown; +} + +/** + * Function signature for linter implementations. + * + * @param specPath - Absolute path to the spec file to lint + * @param options - Optional linter configuration + * @returns Promise resolving to lint results + */ +export type LintFn = (specPath: string, options?: LintOptions) => Promise; + +/** + * Options for differ functions. + */ +export interface DiffOptions { + /** Include metadata-only (patch) changes in results */ + includeMetadata?: boolean; + /** Additional tool-specific options */ + [key: string]: unknown; +} + +/** + * Function signature for differ implementations. + * + * @param oldSpecPath - Absolute path to the old (baseline) spec + * @param newSpecPath - Absolute path to the new (current) spec + * @param options - Optional differ configuration + * @returns Promise resolving to diff results + */ +export type DiffFn = ( + oldSpecPath: string, + newSpecPath: string, + options?: DiffOptions +) => Promise; + +/** + * All possible structural change types for classification. + * + * @remarks + * These categories are used internally by differs to classify raw changes + * before mapping to severity levels. + */ +export type ChangeType = + // Property-level changes + | 'property-added' + | 'property-removed' + | 'required-added' + | 'required-removed' + // Type-level changes + | 'type-changed' + | 'type-narrowed' + | 'type-widened' + // Enum-level changes + | 'enum-value-added' + | 'enum-value-removed' + | 'enum-added' + | 'enum-removed' + // Constraint-level changes + | 'constraint-tightened' + | 'constraint-loosened' + | 'format-changed' + | 'format-added' + | 'format-removed' + // Object-level changes + | 'additional-properties-denied' + | 'additional-properties-allowed' + | 'additional-properties-changed' + | 'property-names-changed' + | 'dependent-required-added' + | 'dependent-required-removed' + | 'dependent-schemas-changed' + | 'unevaluated-properties-changed' + // Array-level changes + | 'items-changed' + | 'min-items-increased' + | 'max-items-decreased' + | 'min-contains-changed' + | 'max-contains-changed' + | 'unevaluated-items-changed' + // Ref-level changes + | 'ref-target-changed' + // Metadata-level changes (patch) + | 'description-changed' + | 'title-changed' + | 'default-changed' + | 'examples-changed' + // Annotation changes (patch per Strands API) + | 'deprecated-changed' + | 'read-only-changed' + | 'write-only-changed' + // Content keyword changes (patch per Strands API) + | 'content-encoding-changed' + | 'content-media-type-changed' + | 'content-schema-changed' + // Granular composition changes (replacing generic composition-changed) + | 'anyof-option-added' + | 'anyof-option-removed' + | 'oneof-option-added' + | 'oneof-option-removed' + | 'allof-member-added' + | 'allof-member-removed' + | 'not-schema-changed' + | 'if-then-else-changed' + // Legacy composition (for backward compat) + | 'composition-changed' + // OpenAPI structural changes + | 'path-added' + | 'path-removed' + | 'operation-added' + | 'operation-removed' + | 'parameter-added' + | 'parameter-required-added' + | 'parameter-removed' + | 'parameter-required-changed' + | 'parameter-schema-changed' + | 'request-body-added' + | 'request-body-removed' + | 'response-added' + | 'response-removed' + | 'response-schema-changed' + | 'security-changed' + | 'server-changed' + // Catch-all for unrecognized changes + | 'unknown-change'; + +/** + * Raw change detected by a differ before severity classification. + * + * @remarks + * This is an internal type used by differ implementations. + * Raw changes are classified into {@link Change} with severity. + */ +export interface RawChange { + /** JSON Pointer path to the change location */ + path: string; + /** Structural change type for classification */ + type: ChangeType; + /** Value in old spec (undefined if added) */ + oldValue?: unknown; + /** Value in new spec (undefined if removed) */ + newValue?: unknown; +} + +/** + * Mapping from change types to their severity classification. + * + * @remarks + * This constant provides the default classification rules aligned with Strands API. + * Breaking changes cause major bumps, non-breaking cause minor, etc. + */ +export const CHANGE_TYPE_SEVERITY: Record = { + // Breaking changes (major) + 'property-removed': 'breaking', + 'required-added': 'breaking', + 'type-changed': 'breaking', + 'type-narrowed': 'breaking', + 'enum-value-removed': 'breaking', + 'enum-added': 'breaking', + 'constraint-tightened': 'breaking', + 'additional-properties-denied': 'breaking', + 'items-changed': 'breaking', + 'min-items-increased': 'breaking', + 'max-items-decreased': 'breaking', + 'ref-target-changed': 'breaking', + 'dependent-required-added': 'breaking', + // Composition breaking changes (per Strands API) + 'anyof-option-added': 'breaking', + 'oneof-option-added': 'breaking', + 'allof-member-added': 'breaking', + 'not-schema-changed': 'breaking', + + // Non-breaking changes (minor) + 'property-added': 'non-breaking', + 'required-removed': 'non-breaking', + 'type-widened': 'non-breaking', + 'enum-value-added': 'non-breaking', + 'enum-removed': 'non-breaking', + 'constraint-loosened': 'non-breaking', + 'additional-properties-allowed': 'non-breaking', + 'additional-properties-changed': 'non-breaking', + 'dependent-required-removed': 'non-breaking', + // Composition non-breaking changes (per Strands API) + 'anyof-option-removed': 'non-breaking', + 'oneof-option-removed': 'non-breaking', + 'allof-member-removed': 'non-breaking', + + // Patch-level changes (per Strands API - format is annotation) + 'format-added': 'patch', + 'format-removed': 'patch', + 'format-changed': 'patch', + 'description-changed': 'patch', + 'title-changed': 'patch', + 'default-changed': 'patch', + 'examples-changed': 'patch', + // Annotation changes (patch per Strands API) + 'deprecated-changed': 'patch', + 'read-only-changed': 'patch', + 'write-only-changed': 'patch', + // Content keyword changes (patch per Strands API) + 'content-encoding-changed': 'patch', + 'content-media-type-changed': 'patch', + 'content-schema-changed': 'patch', + + // Requires manual review (unknown) + 'property-names-changed': 'unknown', + 'dependent-schemas-changed': 'unknown', + 'unevaluated-properties-changed': 'unknown', + 'unevaluated-items-changed': 'unknown', + 'min-contains-changed': 'unknown', + 'max-contains-changed': 'unknown', + 'if-then-else-changed': 'unknown', + 'composition-changed': 'unknown', + // OpenAPI structural changes + 'path-removed': 'breaking', + 'operation-removed': 'breaking', + 'parameter-added': 'non-breaking', + 'parameter-required-added': 'breaking', + 'parameter-removed': 'breaking', + 'parameter-required-changed': 'breaking', + 'request-body-added': 'breaking', + 'response-removed': 'breaking', + 'security-changed': 'breaking', + 'path-added': 'non-breaking', + 'operation-added': 'non-breaking', + 'request-body-removed': 'non-breaking', + 'response-added': 'non-breaking', + 'server-changed': 'non-breaking', + 'parameter-schema-changed': 'unknown', + 'response-schema-changed': 'unknown', + 'unknown-change': 'unknown', +}; + +/** + * Type guard to check if a value is a valid ChangeSeverity. + * + * @param value - The value to check + * @returns True if the value is a valid ChangeSeverity + */ +export function isChangeSeverity(value: unknown): value is ChangeSeverity { + return ( + typeof value === 'string' && ['breaking', 'non-breaking', 'patch', 'unknown'].includes(value) + ); +} + +/** + * Type guard to check if a value is a valid ChangeType. + * + * @param value - The value to check + * @returns True if the value is a valid ChangeType + */ +export function isChangeType(value: unknown): value is ChangeType { + return typeof value === 'string' && value in CHANGE_TYPE_SEVERITY; +} diff --git a/packages/types/index.ts b/packages/types/index.ts new file mode 100644 index 0000000..c23ccde --- /dev/null +++ b/packages/types/index.ts @@ -0,0 +1,3 @@ +export * from './config.js'; +export * from './governance.js'; +export * from './versioning.js'; diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..b9aadfe --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,51 @@ +{ + "name": "@contractual/types", + "private": false, + "version": "0.1.0-beta.0", + "description": "TypeScript type definitions for Contractual - schema contract lifecycle orchestration", + "license": "MIT", + "type": "module", + "sideEffects": false, + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/codotech/contractual.git", + "directory": "packages/types" + }, + "homepage": "https://contractual.dev", + "bugs": { + "url": "https://github.com/codotech/contractual/issues" + }, + "keywords": [ + "contractual", + "types", + "typescript", + "openapi", + "json-schema", + "asyncapi", + "odcs", + "schema", + "contract" + ], + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "prebuild": "pnpm rimraf dist", + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.build.json --noEmit" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/types/tsconfig.build.json b/packages/types/tsconfig.build.json new file mode 100644 index 0000000..973b05e --- /dev/null +++ b/packages/types/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + } +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..5e75f83 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noPropertyAccessFromIndexSignature": true, + "emitDecoratorMetadata": false, + "experimentalDecorators": false + }, + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/versioning.ts b/packages/types/versioning.ts new file mode 100644 index 0000000..8afb1cc --- /dev/null +++ b/packages/types/versioning.ts @@ -0,0 +1,206 @@ +import type { DiffResult, LintResult } from './governance.js'; + +/** + * Semver bump type for version increments. + */ +export type BumpType = 'major' | 'minor' | 'patch'; + +/** + * Version entry for a single contract in the history. + */ +export interface VersionEntry { + /** Semver version string (e.g., "1.2.0") */ + version: string; + /** ISO 8601 timestamp of when this version was released */ + released: string; + /** Changeset filenames that contributed to this version */ + changesets?: string[]; +} + +/** + * Contents of versions.json file for a single contract. + * + * @example + * ```json + * { + * "current": "1.2.0", + * "versions": [ + * { "version": "0.1.0", "released": "2026-02-01T10:00:00Z" }, + * { "version": "1.0.0", "released": "2026-02-05T14:30:00Z", "changesets": ["brave-tigers-fly"] } + * ] + * } + * ``` + */ +export interface ContractVersionsFile { + /** Current (latest) version */ + current: string; + /** History of all versions */ + versions: VersionEntry[]; +} + +/** + * Simple version entry used in the legacy VersionsFile format. + * Contains only the essential version tracking fields. + */ +export interface SimpleVersionEntry { + /** Current version */ + version: string; + /** ISO 8601 timestamp of last release */ + released: string; +} + +/** + * Aggregated versions.json file mapping contract names to their version info. + * + * @remarks + * This is the legacy format used for simple version tracking. + * For richer history tracking, use ContractVersionsFile per contract. + */ +export interface VersionsFile { + [contractName: string]: SimpleVersionEntry; +} + +/** + * Parsed changeset file data. + * + * @example + * ```markdown + * --- + * "orders-api": major + * "order-schema": minor + * --- + * + * ## orders-api + * + * **Breaking changes:** + * - Removed endpoint GET /orders/{id}/details + * ``` + */ +export interface ChangesetFile { + /** Filename without path (e.g., "brave-tigers-fly.md") */ + filename: string; + /** Full path to the changeset file */ + path: string; + /** Map of contract name to bump type */ + bumps: Record; + /** Markdown body with change descriptions */ + body: string; +} + +/** + * Result of applying a version bump to a single contract. + */ +export interface BumpResult { + /** Contract name */ + contract: string; + /** Version before bump */ + oldVersion: string; + /** Version after bump */ + newVersion: string; + /** Type of bump applied */ + bumpType: BumpType; + /** Markdown section describing changes from changeset body */ + changes: string; +} + +/** + * Result of the `contractual version` command. + */ +export interface VersionCommandResult { + /** All version bumps that were applied */ + bumps: BumpResult[]; + /** Filenames of changesets that were consumed */ + consumedChangesets: string[]; + /** Updated changelog content (if generated) */ + changelog?: string; +} + +/** + * Result of the `contractual lint` command. + */ +export interface LintCommandResult { + /** Whether all contracts passed linting (no errors) */ + success: boolean; + /** Individual lint results per contract */ + results: LintResult[]; + /** Total error count across all contracts */ + errorCount: number; + /** Total warning count across all contracts */ + warningCount: number; +} + +/** + * Result of the `contractual breaking` command. + */ +export interface BreakingCommandResult { + /** Whether any breaking changes were detected */ + hasBreaking: boolean; + /** Individual diff results per contract */ + results: DiffResult[]; + /** Suggested overall bump type based on highest severity */ + suggestedBump: BumpType | 'none'; +} + +/** + * Result of the `contractual changeset` command. + */ +export interface ChangesetCommandResult { + /** Whether a changeset was created */ + created: boolean; + /** Path to the created changeset file (if created) */ + path?: string; + /** Filename of the created changeset (if created) */ + filename?: string; + /** Map of contract names to their detected bump types */ + bumps: Record; +} + +/** + * Result of the `contractual status` command. + */ +export interface StatusCommandResult { + /** Current contract versions */ + versions: Record; + /** Pending changesets awaiting consumption */ + pendingChangesets: string[]; + /** Predicted version bumps from pending changesets */ + pendingBumps: Record; + /** Whether there are uncommitted spec changes without changesets */ + hasUncommittedChanges: boolean; +} + +/** + * Type guard to check if a value is a valid BumpType. + * + * @param value - The value to check + * @returns True if the value is a valid BumpType + */ +export function isBumpType(value: unknown): value is BumpType { + return ( + typeof value === 'string' && + ['major', 'minor', 'patch'].includes(value) + ); +} + +/** + * Pre-release state stored in .contractual/pre.json + * + * @example + * ```json + * { + * "tag": "beta", + * "enteredAt": "2026-03-10T10:00:00Z", + * "initialVersions": { + * "orders-api": "1.2.0" + * } + * } + * ``` + */ +export interface PreReleaseState { + /** Pre-release tag (e.g., "alpha", "beta", "rc") */ + tag: string; + /** ISO 8601 timestamp when pre-release mode was entered */ + enteredAt: string; + /** Versions of contracts when pre-release mode was entered */ + initialVersions: Record; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc4a422..abe82b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,620 +4,424 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + micromatch@<4.0.8: 4.0.8 + minimatch@>=9.0.0 <9.0.7: 9.0.7 + yaml@>=2.0.0 <2.8.3: ^2.8.3 + vite@>=7.0.0 <7.3.5: ^7.3.5 + esbuild@>=0.27.3 <0.28.1: 0.28.1 + tar@<7.5.21: 7.5.21 + brace-expansion@>=5.0.0 <5.0.12: 5.0.12 + js-yaml@>=4.0.0 <4.3.2: 4.3.2 + axios@>=1.0.0 <1.20.0: 1.20.0 + pacote@>=21.0.0 <21.5.1: 21.5.1 + smol-toml@<1.9.0: 1.9.0 + nx@>=22.0.0 <22.7.10: 22.7.10 + source-map-js@>=1.0.0 <1.2.2: 1.2.2 + postcss-selector-parser@<7.1.6: 7.1.6 + importers: .: - dependencies: - '@manypkg/cli': - specifier: ^0.21.4 - version: 0.21.4 + devDependencies: '@types/node': - specifier: ^22.10.2 - version: 22.10.7 + specifier: ^22.20.4 + version: 22.20.4 '@typescript-eslint/eslint-plugin': - specifier: ^6.7.5 + specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/parser': - specifier: ^6.7.5 + specifier: ^6.21.0 version: 6.21.0(eslint@8.57.1)(typescript@5.7.3) - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0)) '@vitest/coverage-v8': - specifier: 3.0.3 - version: 3.0.3(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0)) - braces: - specifier: 3.0.3 - version: 3.0.3 + specifier: ^4.1.11 + version: 4.1.11(vitest@4.1.11) eslint: - specifier: ^8.57.0 + specifier: ^8.57.1 version: 8.57.1 eslint-config-prettier: - specifier: ^9.0.0 - version: 9.1.0(eslint@8.57.1) + specifier: ^9.1.2 + version: 9.1.2(eslint@8.57.1) eslint-import-resolver-typescript: - specifier: ^3.6.1 - version: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + specifier: ^3.10.1 + version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-plugin-import: - specifier: ^2.29.1 - version: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-prettier: - specifier: ^5.0.1 - version: 5.2.3(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.2) - follow-redirects: - specifier: 1.15.6 - version: 1.15.6 - ip: - specifier: 2.0.1 - version: 2.0.1 + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.9) + husky: + specifier: ^8.0.3 + version: 8.0.3 + json-schema-typed: + specifier: ^8.0.2 + version: 8.0.2 lerna: - specifier: ^7.3.1 - version: 7.4.2(encoding@0.1.13) + specifier: ^9.0.7 + version: 9.0.7(@types/node@22.20.4) lint-staged: specifier: ^14.0.1 version: 14.0.1(enquirer@2.3.6) - madge: - specifier: ^7.0.0 - version: 7.0.0(typescript@5.7.3) - micromatch: - specifier: 4.0.8 - version: 4.0.8 prettier: - specifier: ^3.2.5 - version: 3.4.2 + specifier: ^3.9.9 + version: 3.9.9 rimraf: - specifier: ^5.0.5 - version: 5.0.10 - rxjs: - specifier: ^7.8.1 - version: 7.8.1 - tar: - specifier: 6.2.0 - version: 6.2.0 - ts-jest: - specifier: ^29.1.3 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))(typescript@5.7.3) - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@types/node@22.10.7)(typescript@5.7.3) + specifier: ^6.1.3 + version: 6.1.3 typescript: - specifier: ~5.7.2 + specifier: ~5.7.3 version: 5.7.3 + vite: + specifier: ^7.3.5 + version: 7.3.6(@types/node@22.20.4)(yaml@2.9.1) vitest: - specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) + yaml: + specifier: ^2.8.3 + version: 2.9.1 + + packages/changesets: + dependencies: + '@contractual/types': + specifier: workspace:* + version: link:../types + semver: + specifier: ^7.8.5 + version: 7.8.5 + yaml: + specifier: ^2.8.3 + version: 2.9.1 devDependencies: - husky: - specifier: ^8.0.3 - version: 8.0.3 + '@types/semver': + specifier: ^7.8.0 + version: 7.8.0 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) packages/cli: dependencies: - '@contractual/generators.contract': + '@contractual/changesets': specifier: workspace:* - version: link:../generators/contract - '@contractual/generators.diff': + version: link:../changesets + '@contractual/governance': specifier: workspace:* - version: link:../generators/diff - '@contractual/generators.spec': + version: link:../governance + '@contractual/types': specifier: workspace:* - version: link:../generators/spec + version: link:../types + '@inquirer/prompts': + specifier: ^8.7.2 + version: 8.7.2(@types/node@22.20.4) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) chalk: - specifier: ^5.4.1 - version: 5.4.1 + specifier: ^6.0.0 + version: 6.0.0 commander: specifier: ^12.1.0 version: 12.1.0 - inquirer: - specifier: ^12.3.2 - version: 12.3.2(@types/node@22.10.7) + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 ora: - specifier: ^8.1.1 - version: 8.1.1 + specifier: ^9.4.1 + version: 9.4.1 + yaml: + specifier: ^2.8.3 + version: 2.9.1 devDependencies: - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0)) + '@vitest/coverage-v8': + specifier: ^4.1.11 + version: 4.1.11(vitest@4.1.11) vitest: - specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) - - packages/contract: - dependencies: - '@ts-rest/core': - specifier: ^3.51.0 - version: 3.51.0(@types/node@22.10.7)(zod@3.24.1) - axios: - specifier: ^1.7.9 - version: 1.7.9 - zod: - specifier: ^3.24.1 - version: 3.24.1 - - packages/generators/contract: - dependencies: - '@apidevtools/swagger-parser': - specifier: ^10.1.1 - version: 10.1.1(openapi-types@12.1.3) - chalk: - specifier: ^5.4.1 - version: 5.4.1 - handlebars: - specifier: ^4.7.8 - version: 4.7.8 - openapi-types: - specifier: ^12.1.3 - version: 12.1.3 - openapi-zod-client: - specifier: ^1.18.2 - version: 1.18.2 - openapi3-ts: - specifier: ^4.4.0 - version: 4.4.0 + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) + + packages/differs.core: + dependencies: + '@contractual/types': + specifier: workspace:* + version: link:../types devDependencies: - ora: - specifier: ^8.1.1 - version: 8.1.1 - typescript: - specifier: ~5.7.2 - version: 5.7.3 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 - packages/generators/diff: + packages/differs.json-schema: dependencies: - chalk: - specifier: ^5.4.1 - version: 5.4.1 - openapi-diff: - specifier: ^0.23.7 - version: 0.23.7(openapi-types@12.1.3) - openapi-types: - specifier: ^12.1.3 - version: 12.1.3 - semver: - specifier: ^7.6.3 - version: 7.6.3 - table: - specifier: ^6.9.0 - version: 6.9.0 + '@contractual/differs.core': + specifier: workspace:* + version: link:../differs.core + '@contractual/types': + specifier: workspace:* + version: link:../types devDependencies: - '@types/semver': - specifier: ^7.5.8 - version: 7.5.8 - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0)) - typescript: - specifier: ~5.7.2 - version: 5.7.3 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 vitest: - specifier: ^3.0.3 - version: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) - packages/generators/spec: + packages/differs.openapi: dependencies: - '@contractual/generators.diff': + '@contractual/differs.core': specifier: workspace:* - version: link:../diff - '@typespec/compiler': - specifier: ^0.63.0 - version: 0.63.0 - '@typespec/http': - specifier: ^0.63.0 - version: 0.63.0(@typespec/compiler@0.63.0) - '@typespec/openapi': - specifier: ^0.63.0 - version: 0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0)) - '@typespec/openapi3': - specifier: ^0.63.0 - version: 0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0))(@typespec/openapi@0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0)))(@typespec/versioning@0.63.0(@typespec/compiler@0.63.0)) - '@typespec/rest': - specifier: ^0.63.1 - version: 0.63.1(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0)) - '@typespec/versioning': - specifier: ^0.63.0 - version: 0.63.0(@typespec/compiler@0.63.0) - semver: - specifier: ^7.6.3 - version: 7.6.3 - yaml: - specifier: ^2.7.0 - version: 2.7.0 + version: link:../differs.core + '@contractual/types': + specifier: workspace:* + version: link:../types + '@redocly/openapi-core': + specifier: ^1.34.20 + version: 1.34.20 devDependencies: - '@types/semver': - specifier: ^7.5.8 - version: 7.5.8 - chalk: - specifier: ^5.4.1 - version: 5.4.1 - inquirer: - specifier: ^12.3.2 - version: 12.3.2(@types/node@22.10.7) - openapi-types: - specifier: ^12.1.3 - version: 12.1.3 - ora: - specifier: ^8.1.1 - version: 8.1.1 - typescript: - specifier: ~5.7.2 - version: 5.7.3 - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@apidevtools/json-schema-ref-parser@11.7.2': - resolution: {integrity: sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==} - engines: {node: '>= 16'} - - '@apidevtools/json-schema-ref-parser@9.0.9': - resolution: {integrity: sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==} - - '@apidevtools/json-schema-ref-parser@9.1.2': - resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} - - '@apidevtools/openapi-schemas@2.1.0': - resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} - engines: {node: '>=10'} - - '@apidevtools/swagger-methods@3.0.2': - resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - - '@apidevtools/swagger-parser@10.0.3': - resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} - peerDependencies: - openapi-types: '>=7' - - '@apidevtools/swagger-parser@10.1.1': - resolution: {integrity: sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==} - peerDependencies: - openapi-types: '>=7' - - '@babel/code-frame@7.25.9': - resolution: {integrity: sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.26.5': - resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.26.0': - resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.26.5': - resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} - engines: {node: '>=6.9.0'} + packages/governance: + dependencies: + '@contractual/differs.json-schema': + specifier: workspace:* + version: link:../differs.json-schema + '@contractual/differs.openapi': + specifier: workspace:* + version: link:../differs.openapi + '@contractual/types': + specifier: workspace:* + version: link:../types + ajv: + specifier: ^8.20.0 + version: 8.20.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) + devDependencies: + '@redocly/openapi-core': + specifier: ^1.34.20 + version: 1.34.20 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} + packages/types: {} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} +packages: - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.0': - resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.25.9': - resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.5': - resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.26.0': - resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.25.9': - resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.25.9': - resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime@7.26.0': - resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} - engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} - '@babel/template@7.25.9': - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} - engines: {node: '>=6.9.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@babel/traverse@7.26.5': - resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==} - engines: {node: '>=6.9.0'} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@babel/types@7.26.5': - resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - '@dependents/detective-less@4.1.0': - resolution: {integrity: sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg==} - engines: {node: '>=14'} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@2.1.4': @@ -628,8 +432,9 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@gar/promisify@1.1.3': - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} @@ -640,10 +445,6 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/momoa@2.0.4': - resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} - engines: {node: '>=10.10.0'} - '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead @@ -652,643 +453,719 @@ packages: resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} - '@inquirer/checkbox@4.0.6': - resolution: {integrity: sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/ansi@2.0.8': + resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/confirm@5.1.3': - resolution: {integrity: sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA==} - engines: {node: '>=18'} + '@inquirer/checkbox@5.2.5': + resolution: {integrity: sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/core@10.1.4': - resolution: {integrity: sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/editor@4.2.3': - resolution: {integrity: sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==} - engines: {node: '>=18'} + '@inquirer/confirm@6.3.2': + resolution: {integrity: sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/expand@4.0.6': - resolution: {integrity: sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==} + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/figures@1.0.9': - resolution: {integrity: sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==} - engines: {node: '>=18'} + '@inquirer/core@12.0.3': + resolution: {integrity: sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/input@4.1.3': - resolution: {integrity: sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==} + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/number@3.0.6': - resolution: {integrity: sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==} - engines: {node: '>=18'} + '@inquirer/editor@5.3.3': + resolution: {integrity: sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/password@4.0.6': - resolution: {integrity: sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==} + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/prompts@7.2.3': - resolution: {integrity: sha512-hzfnm3uOoDySDXfDNOm9usOuYIaQvTgKp/13l1uJoe6UNY+Zpcn2RYt0jXz3yA+yemGHvDOxVzqWl3S5sQq53Q==} - engines: {node: '>=18'} + '@inquirer/expand@5.1.5': + resolution: {integrity: sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/rawlist@4.0.6': - resolution: {integrity: sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/search@3.0.6': - resolution: {integrity: sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==} + '@inquirer/external-editor@3.0.5': + resolution: {integrity: sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/figures@2.0.9': + resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/select@4.0.6': - resolution: {integrity: sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==} + '@inquirer/input@5.1.6': + resolution: {integrity: sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.2.3': + resolution: {integrity: sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@inquirer/type@3.0.2': - resolution: {integrity: sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@inquirer/password@5.2.2': + resolution: {integrity: sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + '@inquirer/prompts@8.7.2': + resolution: {integrity: sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/rawlist@5.3.5': + resolution: {integrity: sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + '@types/node': '>=18' peerDependenciesMeta: - node-notifier: + '@types/node': optional: true - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/search@4.3.3': + resolution: {integrity: sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/select@5.2.5': + resolution: {integrity: sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@inquirer/type@4.1.1': + resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + '@types/node': '>=18' peerDependenciesMeta: - node-notifier: + '@types/node': optional: true - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@isaacs/string-locale-compare@1.1.0': + resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/diff-sequences@30.4.0': + resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jest/schemas@30.4.1': + resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@jsdevtools/ono@7.1.3': - resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} - '@lerna/child-process@7.4.2': - resolution: {integrity: sha512-je+kkrfcvPcwL5Tg8JRENRqlbzjdlZXyaR88UcnCdNW0AJ1jX9IfHRys1X7AwSroU2ug8ESNC+suoBw1vX833Q==} - engines: {node: '>=16.0.0'} + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - '@lerna/create@7.4.2': - resolution: {integrity: sha512-1wplFbQ52K8E/unnqB0Tq39Z4e+NEoNrpovEnl6GpsTUrC6WDp8+w0Le2uCBV0hXyemxChduCkLz4/y1H1wTeg==} - engines: {node: '>=16.0.0'} + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} - '@liuli-util/fs-extra@0.1.0': - resolution: {integrity: sha512-eaAyDyMGT23QuRGbITVY3SOJff3G9ekAAyGqB9joAnTBmqvFN+9a1FazOdO70G6IUqgpKV451eBHYSRcOJ/FNQ==} + '@npmcli/agent@4.0.2': + resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} + engines: {node: ^20.17.0 || >=22.9.0} - '@manypkg/cli@0.21.4': - resolution: {integrity: sha512-EACxxb+c/t6G0l1FrlyewZeBnyR5V1cLkXjnBfsay5TN1UgbilFpG6POglzn+lVJet9NqnEKe3RLHABzkIDZ0Q==} - engines: {node: '>=14.18.0'} + '@npmcli/arborist@9.1.6': + resolution: {integrity: sha512-c5Pr3EG8UP5ollkJy2x+UdEQC5sEHe3H9whYn6hb2HJimAKS4zmoJkx5acCiR/g4P38RnCSMlsYQyyHnKYeLvQ==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true - '@manypkg/find-root@2.2.3': - resolution: {integrity: sha512-jtEZKczWTueJYHjGpxU3KJQ08Gsrf4r6Q2GjmPp/RGk5leeYAA1eyDADSAF+KVCsQ6EwZd/FMcOFCoMhtqdCtQ==} - engines: {node: '>=14.18.0'} + '@npmcli/fs@4.0.0': + resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} + engines: {node: ^18.17.0 || >=20.5.0} - '@manypkg/get-packages@2.2.2': - resolution: {integrity: sha512-3+Zd8kLZmsyJFmWTBtY0MAuCErI7yKB2cjMBlujvSVKZ2R/BMXi0kjCXu2dtRlSq/ML86t1FkumT0yreQ3n8OQ==} - engines: {node: '>=14.18.0'} + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} - '@manypkg/tools@1.1.2': - resolution: {integrity: sha512-3lBouSuF7CqlseLB+FKES0K4FQ02JrbEoRtJhxnsyB1s5v4AP03gsoohN8jp7DcOImhaR9scYdztq3/sLfk/qQ==} - engines: {node: '>=14.18.0'} + '@npmcli/git@7.0.2': + resolution: {integrity: sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==} + engines: {node: ^20.17.0 || >=22.9.0} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@npmcli/installed-package-contents@3.0.0': + resolution: {integrity: sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@npmcli/installed-package-contents@4.0.0': + resolution: {integrity: sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@npmcli/map-workspaces@5.0.3': + resolution: {integrity: sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==} + engines: {node: ^20.17.0 || >=22.9.0} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} + '@npmcli/metavuln-calculator@9.0.3': + resolution: {integrity: sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==} + engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/fs@2.1.2': - resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + '@npmcli/name-from-folder@3.0.0': + resolution: {integrity: sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==} + engines: {node: ^18.17.0 || >=20.5.0} - '@npmcli/fs@3.1.1': - resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@npmcli/name-from-folder@4.0.0': + resolution: {integrity: sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==} + engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/git@4.1.0': - resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@npmcli/node-gyp@4.0.0': + resolution: {integrity: sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==} + engines: {node: ^18.17.0 || >=20.5.0} - '@npmcli/installed-package-contents@2.1.0': - resolution: {integrity: sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true + '@npmcli/node-gyp@5.0.0': + resolution: {integrity: sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==} + engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/move-file@2.0.1': - resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This functionality has been moved to @npmcli/fs + '@npmcli/package-json@7.0.2': + resolution: {integrity: sha512-0ylN3U5htO1SJTmy2YI78PZZjLkKUGg7EKgukb2CRi0kzyoDr0cfjHAzi7kozVhj2V3SxN1oyKqZ2NSo40z00g==} + engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/node-gyp@3.0.0': - resolution: {integrity: sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@npmcli/promise-spawn@9.0.1': + resolution: {integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==} + engines: {node: ^20.17.0 || >=22.9.0} - '@npmcli/promise-spawn@6.0.2': - resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@npmcli/query@4.0.1': + resolution: {integrity: sha512-4OIPFb4weUUwkDXJf4Hh1inAn8neBGq3xsH4ZsAaN6FK3ldrFkH7jSpCc7N9xesi0Sp+EBXJ9eGMDrEww2Ztqw==} + engines: {node: ^18.17.0 || >=20.5.0} - '@npmcli/run-script@6.0.2': - resolution: {integrity: sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@npmcli/redact@3.2.2': + resolution: {integrity: sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==} + engines: {node: ^18.17.0 || >=20.5.0} - '@nrwl/devkit@16.10.0': - resolution: {integrity: sha512-fRloARtsDQoQgQ7HKEy0RJiusg/HSygnmg4gX/0n/Z+SUS+4KoZzvHjXc6T5ZdEiSjvLypJ+HBM8dQzIcVACPQ==} + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} - '@nrwl/tao@16.10.0': - resolution: {integrity: sha512-QNAanpINbr+Pod6e1xNgFbzK1x5wmZl+jMocgiEFXZ67KHvmbD6MAQQr0MMz+GPhIu7EE4QCTLTyCEMlAG+K5Q==} - hasBin: true + '@npmcli/run-script@10.0.3': + resolution: {integrity: sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==} + engines: {node: ^20.17.0 || >=22.9.0} - '@nx/devkit@16.10.0': - resolution: {integrity: sha512-IvKQqRJFDDiaj33SPfGd3ckNHhHi6ceEoqCbAP4UuMXOPPVOX6H0KVk+9tknkPb48B7jWIw6/AgOeWkBxPRO5w==} + '@nx/devkit@22.7.7': + resolution: {integrity: sha512-XU9LZuqT4+/2DTrpKnGDuGFKjwe7LRr5kMfT6EOPiPYCYkmtwXriMGiaRZNabb6kliVG3L34L7+TEiMUWRPCDQ==} peerDependencies: - nx: '>= 15 <= 17' + nx: 22.7.10 - '@nx/nx-darwin-arm64@16.10.0': - resolution: {integrity: sha512-YF+MIpeuwFkyvM5OwgY/rTNRpgVAI/YiR0yTYCZR+X3AAvP775IVlusNgQ3oedTBRUzyRnI4Tknj1WniENFsvQ==} - engines: {node: '>= 10'} + '@nx/nx-darwin-arm64@22.7.10': + resolution: {integrity: sha512-gy0ASfsnFsMmTLQaGTQyf8l95kAgawBpdMrgtxsnXzyKAasqzfIEhEbb74/6+DmsmcnNyfBvAKvVvFBiNs3ZYw==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@16.10.0': - resolution: {integrity: sha512-ypi6YxwXgb0kg2ixKXE3pwf5myVNUgWf1CsV5OzVccCM8NzheMO51KDXTDmEpXdzUsfT0AkO1sk5GZeCjhVONg==} - engines: {node: '>= 10'} + '@nx/nx-darwin-x64@22.7.10': + resolution: {integrity: sha512-IGNSVU85ktrNm9cH9yLgSaQjmRVCcEldaZwtAqNaMcNU+Go5xY4zYv8tDQ2MJ3hXpi6pNdcFzZp8tZIJliHyPA==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@16.10.0': - resolution: {integrity: sha512-UeEYFDmdbbDkTQamqvtU8ibgu5jQLgFF1ruNb/U4Ywvwutw2d4ruOMl2e0u9hiNja9NFFAnDbvzrDcMo7jYqYw==} - engines: {node: '>= 10'} + '@nx/nx-freebsd-x64@22.7.10': + resolution: {integrity: sha512-KlmBmvD8uE7Saah3DO61D7ywn08FtYJE0I0AwZGs+0cisY1bUawtZZNYvbPTprHaCOWvbU2GeIVstK34vmbVXg==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@16.10.0': - resolution: {integrity: sha512-WV3XUC2DB6/+bz1sx+d1Ai9q2Cdr+kTZRN50SOkfmZUQyEBaF6DRYpx/a4ahhxH3ktpNfyY8Maa9OEYxGCBkQA==} - engines: {node: '>= 10'} + '@nx/nx-linux-arm-gnueabihf@22.7.10': + resolution: {integrity: sha512-xwCv9gR968hMeeWUqBneLR9nBqP+ECPVFumdWCxMBh116ftCjMMik8E3BdUKLdqreVYFJdwvJRPortHj/YYikA==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@16.10.0': - resolution: {integrity: sha512-aWIkOUw995V3ItfpAi5FuxQ+1e9EWLS1cjWM1jmeuo+5WtaKToJn5itgQOkvSlPz+HSLgM3VfXMvOFALNk125g==} - engines: {node: '>= 10'} + '@nx/nx-linux-arm64-gnu@22.7.10': + resolution: {integrity: sha512-AiwBgerLicRzcyEevqwDuP22Nj5nFFrjJsCeZ+D4rC2HmS4LdBzXb+BBuHAZTysCo9QClk1t9VuVwwKyorU7ZA==} cpu: [arm64] os: [linux] - '@nx/nx-linux-arm64-musl@16.10.0': - resolution: {integrity: sha512-uO6Gg+irqpVcCKMcEPIQcTFZ+tDI02AZkqkP7koQAjniLEappd8DnUBSQdcn53T086pHpdc264X/ZEpXFfrKWQ==} - engines: {node: '>= 10'} + '@nx/nx-linux-arm64-musl@22.7.10': + resolution: {integrity: sha512-8GNi8v6Ej/TyAaVBiFRAwFYrqdje/dyFbPzpq895m3ow8zbPrYmoz/qizrgkRzBFulgkSxu33LXi388reVLi2w==} cpu: [arm64] os: [linux] - '@nx/nx-linux-x64-gnu@16.10.0': - resolution: {integrity: sha512-134PW/u/arNFAQKpqMJniC7irbChMPz+W+qtyKPAUXE0XFKPa7c1GtlI/wK2dvP9qJDZ6bKf0KtA0U/m2HMUOA==} - engines: {node: '>= 10'} + '@nx/nx-linux-x64-gnu@22.7.10': + resolution: {integrity: sha512-spcDvoQK/mrSvnHFdophlwgzUoa8lYXAZTk15+w/gLpaowP5hR4mInhUWsU9VuEoe41J3x+FcFwmw01Djj6KNQ==} cpu: [x64] os: [linux] - '@nx/nx-linux-x64-musl@16.10.0': - resolution: {integrity: sha512-q8sINYLdIJxK/iUx9vRk5jWAWb/2O0PAbOJFwv4qkxBv4rLoN7y+otgCZ5v0xfx/zztFgk/oNY4lg5xYjIso2Q==} - engines: {node: '>= 10'} + '@nx/nx-linux-x64-musl@22.7.10': + resolution: {integrity: sha512-76PdpGIQ1PVA77sYM4Ons779Yx7y3H+Hlsax2HhifAv8dKhAz9/Lmhst9ZqS2zU5v03lGEnN77W4KZQHJBiltA==} cpu: [x64] os: [linux] - '@nx/nx-win32-arm64-msvc@16.10.0': - resolution: {integrity: sha512-moJkL9kcqxUdJSRpG7dET3UeLIciwrfP08mzBQ12ewo8K8FzxU8ZUsTIVVdNrwt01CXOdXoweGfdQLjJ4qTURA==} - engines: {node: '>= 10'} + '@nx/nx-win32-arm64-msvc@22.7.10': + resolution: {integrity: sha512-dARAojqVyvE88Mt1ms6+UTb3TIAoo4xzkPaX05Qcmh/c8+SQgA24JuqiRp+4kRz1voYwFexRpWpcrfpKJ0Nr+Q==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@16.10.0': - resolution: {integrity: sha512-5iV2NKZnzxJwZZ4DM5JVbRG/nkhAbzEskKaLBB82PmYGKzaDHuMHP1lcPoD/rtYMlowZgNA/RQndfKvPBPwmXA==} - engines: {node: '>= 10'} + '@nx/nx-win32-x64-msvc@22.7.10': + resolution: {integrity: sha512-qlH70TBMrRw/5pMg6MtPXkuM3LzDHszpqWcJ4NPdipLsyD6T+B+NteanlPtxZrdfqi+8Hd/7fv2OkllA8iY1fg==} cpu: [x64] os: [win32] - '@octokit/auth-token@3.0.4': - resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==} - engines: {node: '>= 14'} + '@octokit/auth-token@4.0.0': + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} - '@octokit/core@4.2.4': - resolution: {integrity: sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==} - engines: {node: '>= 14'} + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} + engines: {node: '>= 18'} - '@octokit/endpoint@7.0.6': - resolution: {integrity: sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==} - engines: {node: '>= 14'} + '@octokit/endpoint@9.0.6': + resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} + engines: {node: '>= 18'} - '@octokit/graphql@5.0.6': - resolution: {integrity: sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==} - engines: {node: '>= 14'} + '@octokit/graphql@7.1.1': + resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} + engines: {node: '>= 18'} - '@octokit/openapi-types@18.1.1': - resolution: {integrity: sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==} + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} '@octokit/plugin-enterprise-rest@6.0.1': resolution: {integrity: sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==} - '@octokit/plugin-paginate-rest@6.1.2': - resolution: {integrity: sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==} - engines: {node: '>= 14'} + '@octokit/plugin-paginate-rest@11.4.4-cjs.2': + resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} + engines: {node: '>= 18'} peerDependencies: - '@octokit/core': '>=4' + '@octokit/core': '5' - '@octokit/plugin-request-log@1.0.4': - resolution: {integrity: sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==} + '@octokit/plugin-request-log@4.0.1': + resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + engines: {node: '>= 18'} peerDependencies: - '@octokit/core': '>=3' + '@octokit/core': '5' - '@octokit/plugin-rest-endpoint-methods@7.2.3': - resolution: {integrity: sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==} - engines: {node: '>= 14'} + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1': + resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} + engines: {node: '>= 18'} peerDependencies: - '@octokit/core': '>=3' - - '@octokit/request-error@3.0.3': - resolution: {integrity: sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==} - engines: {node: '>= 14'} - - '@octokit/request@6.2.8': - resolution: {integrity: sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==} - engines: {node: '>= 14'} - - '@octokit/rest@19.0.11': - resolution: {integrity: sha512-m2a9VhaP5/tUw8FwfnW2ICXlXpLPIqxtg3XcAiGMLj/Xhw3RSBfZ8le/466ktO1Gcjr8oXudGnHhxV1TXJgFxw==} - engines: {node: '>= 14'} - - '@octokit/tsconfig@1.0.2': - resolution: {integrity: sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==} + '@octokit/core': ^5 - '@octokit/types@10.0.0': - resolution: {integrity: sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==} + '@octokit/request-error@5.1.1': + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} - '@octokit/types@9.3.2': - resolution: {integrity: sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==} + '@octokit/request@8.4.1': + resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} + engines: {node: '>= 18'} - '@parcel/watcher@2.0.4': - resolution: {integrity: sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==} - engines: {node: '>= 10.0.0'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@pkgr/core@0.1.1': - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - - '@pnpm/config.env-replace@1.1.0': - resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} - engines: {node: '>=12.22.0'} + '@octokit/rest@20.1.2': + resolution: {integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==} + engines: {node: '>= 18'} - '@pnpm/network.ca-file@1.0.2': - resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} - engines: {node: '>=12.22.0'} - - '@pnpm/npm-conf@2.3.1': - resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} - engines: {node: '>=12'} + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} - '@readme/better-ajv-errors@1.6.0': - resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==} - engines: {node: '>=14'} - peerDependencies: - ajv: 4.11.8 - 8 + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} - '@readme/json-schema-ref-parser@1.2.0': - resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==} + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} - '@readme/openapi-parser@2.6.0': - resolution: {integrity: sha512-pyFJXezWj9WI1O+gdp95CoxfY+i+Uq3kKk4zXIFuRAZi9YnHpHOpjumWWr67wkmRTw19Hskh9spyY0Iyikf3fA==} - engines: {node: '>=18'} - peerDependencies: - openapi-types: '>=7' + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} - '@readme/openapi-schemas@3.1.0': - resolution: {integrity: sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==} - engines: {node: '>=18'} + '@redocly/openapi-core@1.34.20': + resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} - '@rollup/rollup-android-arm-eabi@4.31.0': - resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==} + '@rollup/rollup-android-arm-eabi@4.62.3': + resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.31.0': - resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==} + '@rollup/rollup-android-arm64@4.62.3': + resolution: {integrity: sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.31.0': - resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==} + '@rollup/rollup-darwin-arm64@4.62.3': + resolution: {integrity: sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.31.0': - resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==} + '@rollup/rollup-darwin-x64@4.62.3': + resolution: {integrity: sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.31.0': - resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==} + '@rollup/rollup-freebsd-arm64@4.62.3': + resolution: {integrity: sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.31.0': - resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==} + '@rollup/rollup-freebsd-x64@4.62.3': + resolution: {integrity: sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.31.0': - resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + resolution: {integrity: sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.31.0': - resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==} + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + resolution: {integrity: sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.31.0': - resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==} + '@rollup/rollup-linux-arm64-gnu@4.62.3': + resolution: {integrity: sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.31.0': - resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==} + '@rollup/rollup-linux-arm64-musl@4.62.3': + resolution: {integrity: sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.31.0': - resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==} + '@rollup/rollup-linux-loong64-gnu@4.62.3': + resolution: {integrity: sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.3': + resolution: {integrity: sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': - resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==} + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + resolution: {integrity: sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.3': + resolution: {integrity: sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.31.0': - resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==} + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + resolution: {integrity: sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.31.0': - resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==} + '@rollup/rollup-linux-riscv64-musl@4.62.3': + resolution: {integrity: sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + resolution: {integrity: sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.31.0': - resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} + '@rollup/rollup-linux-x64-gnu@4.62.3': + resolution: {integrity: sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.31.0': - resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==} + '@rollup/rollup-linux-x64-musl@4.62.3': + resolution: {integrity: sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.31.0': - resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==} + '@rollup/rollup-openbsd-x64@4.62.3': + resolution: {integrity: sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.3': + resolution: {integrity: sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + resolution: {integrity: sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.31.0': - resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==} + '@rollup/rollup-win32-ia32-msvc@4.62.3': + resolution: {integrity: sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.31.0': - resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==} + '@rollup/rollup-win32-x64-gnu@4.62.3': + resolution: {integrity: sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.3': + resolution: {integrity: sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sigstore/bundle@1.1.0': - resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@sigstore/protobuf-specs@0.2.1': - resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@sigstore/sign@1.0.0': - resolution: {integrity: sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@sigstore/tuf@1.0.3': - resolution: {integrity: sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@sindresorhus/is@5.6.0': - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@szmarczak/http-timer@5.0.1': - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - - '@ts-rest/core@3.51.0': - resolution: {integrity: sha512-v6lnWEcpZj1UgN9wb84XQ+EORP1QEtncFumoXMJjno5ZUV6vdjKze3MYcQN0C6vjBpIJPQEaI/gab2jr4/0KzQ==} - peerDependencies: - '@types/node': ^18.18.7 || >=20.8.4 - zod: ^3.22.3 - peerDependenciesMeta: - '@types/node': - optional: true - zod: - optional: true - - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + '@sigstore/core@3.2.1': + resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} + engines: {node: ^20.17.0 || >=22.9.0} - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@tufjs/canonical-json@1.0.0': - resolution: {integrity: sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@sigstore/protobuf-specs@0.5.1': + resolution: {integrity: sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==} + engines: {node: ^18.17.0 || >=20.5.0} - '@tufjs/models@1.0.4': - resolution: {integrity: sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@sigstore/sign@4.1.1': + resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==} + engines: {node: ^20.17.0 || >=22.9.0} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@sigstore/tuf@4.0.2': + resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==} + engines: {node: ^20.17.0 || >=22.9.0} - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@sigstore/verify@3.1.1': + resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} + engines: {node: ^20.17.0 || >=22.9.0} - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} - '@types/fs-extra@9.0.13': - resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@tufjs/models@4.1.0': + resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} + engines: {node: ^20.17.0 || >=22.9.0} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1296,32 +1173,17 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/minimatch@3.0.5': - resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - '@types/node@22.10.7': - resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/semver@6.2.7': - resolution: {integrity: sha512-blctEWbzUFzQx799RZjzzIdBJOXmE37YYEyDtKkx5Dg+V7o/zyyAxLPiI98A2jdTtDgxZleMdfV+7p8WbRJ1OQ==} - - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} @@ -1358,23 +1220,10 @@ packages: typescript: optional: true - '@typescript-eslint/types@5.62.0': - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/types@6.21.0': resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/typescript-estree@5.62.0': - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@6.21.0': resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1390,145 +1239,187 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/visitor-keys@5.62.0': - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/visitor-keys@6.21.0': resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} - '@typespec/compiler@0.63.0': - resolution: {integrity: sha512-cC3YniwbFghn1fASX3r1IgNjMrwaY4gmzznkHT4f/NxE+HK4XoXWn4EG7287QgVMCaHUykzJCIfW9k7kIleW5A==} - engines: {node: '>=18.0.0'} - hasBin: true + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} - '@typespec/http@0.63.0': - resolution: {integrity: sha512-SYVbBmLPAPdWZfdMs0QlbpTnFREDnkINu2FR+0kRX12qzbRgpRbLsdhg59qx4TfKoh4IAPgSV+Fq84w7BWGsyQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@typespec/compiler': ~0.63.0 - '@typespec/streams': ~0.63.0 - peerDependenciesMeta: - '@typespec/streams': - optional: true + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] - '@typespec/openapi3@0.63.0': - resolution: {integrity: sha512-HC8VeakPznXNn7euAyAxUFNsOcfSzM8tQwYPNUMWs0qGJqGgb6vjf5rShQmfgrCe5Y6zcMM2PPBuxaFV3xXYLw==} - engines: {node: '>=18.0.0'} - hasBin: true - peerDependencies: - '@typespec/compiler': ~0.63.0 - '@typespec/http': ~0.63.0 - '@typespec/openapi': ~0.63.0 - '@typespec/versioning': ~0.63.0 - '@typespec/xml': '*' - peerDependenciesMeta: - '@typespec/xml': - optional: true + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] - '@typespec/openapi@0.63.0': - resolution: {integrity: sha512-/KzR60mj3P/LnNWd/QfH0KTN/If4+mjrsWNSB7/uab6c8Qu/lNsGlZDkmWq4EFiwBR7VmpdFz9FP7d/m3O+tGw==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@typespec/compiler': ~0.63.0 - '@typespec/http': ~0.63.0 + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] - '@typespec/rest@0.63.1': - resolution: {integrity: sha512-RQbTM+HGjCaNIWC0v72m5ulnuvLjuRigb7pH4QeRCvFtGPHos+WBv5SImkGrbYx3353OGR8dIi7lWe7aNwiDcQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@typespec/compiler': ~0.63.0 - '@typespec/http': ~0.63.0 + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] - '@typespec/versioning@0.63.0': - resolution: {integrity: sha512-BPvmPL+g20yEmSA8XRfbIHdToNOjssq4QfwOU6D7kKLLXnZHFb1hmuwW0tf0Wa/lYgoaUC60ONAeoXgNT1ZOIQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@typespec/compiler': ~0.63.0 + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] - '@ungap/structured-clone@1.2.1': - resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] - '@vitest/coverage-c8@0.33.0': - resolution: {integrity: sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==} - deprecated: v8 coverage is moved to @vitest/coverage-v8 package - peerDependencies: - vitest: '>=0.30.0 <1' + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] - '@vitest/coverage-v8@3.0.3': - resolution: {integrity: sha512-uVbJ/xhImdNtzPnLyxCZJMTeTIYdgcC2nWtBBBpR1H6z0w8m7D+9/zrDIx2nNxgMg9r+X8+RY2qVpUDeW2b3nw==} + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: - '@vitest/browser': 3.0.3 - vitest: 3.0.3 + '@vitest/browser': 4.1.11 + vitest: 4.1.11 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@3.0.3': - resolution: {integrity: sha512-SbRCHU4qr91xguu+dH3RUdI5dC86zm8aZWydbp961aIR7G8OYNN6ZiayFuf9WAngRbFOfdrLHCGgXTj3GtoMRQ==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@3.0.3': - resolution: {integrity: sha512-XT2XBc4AN9UdaxJAeIlcSZ0ILi/GzmG5G8XSly4gaiqIvPV3HMTSIDZWJVX6QRJ0PX1m+W8Cy0K9ByXNb/bPIA==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 + vite: ^7.3.5 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@3.0.3': - resolution: {integrity: sha512-gCrM9F7STYdsDoNjGgYXKPq4SkSxwwIU5nkaQvdUxiQ0EcNlez+PdKOVIsUJvh9P9IeIFmjn4IIREWblOBpP2Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@3.0.3': - resolution: {integrity: sha512-Rgi2kOAk5ZxWZlwPguRJFOBmWs6uvvyAAR9k3MvjRvYrG7xYvKChZcmnnpJCS98311CBDMqsW9MzzRFsj2gX3g==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@3.0.3': - resolution: {integrity: sha512-kNRcHlI4txBGztuJfPEJ68VezlPAXLRT1u5UCx219TU3kOG2DplNxhWLwDf2h6emwmTPogzLnGVwP6epDaJN6Q==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@3.0.3': - resolution: {integrity: sha512-7/dgux8ZBbF7lEIKNnEqQlyRaER9nkAL9eTmdKJkDO3hS8p59ATGwKOCUDHcBLKr7h/oi/6hP+7djQk8049T2A==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@3.0.3': - resolution: {integrity: sha512-f+s8CvyzPtMFY1eZKkIHGhPsQgYo5qCm6O8KZoim9qm1/jT64qBgGpO5tHscNH6BzRHM+edLNOP+3vO8+8pE/A==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} - '@yarnpkg/parsers@3.0.0-rc.46': - resolution: {integrity: sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==} - engines: {node: '>=14.15.0'} - - '@zkochan/js-yaml@0.0.6': - resolution: {integrity: sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==} + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} hasBin: true - '@zodios/core@10.9.6': - resolution: {integrity: sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==} - peerDependencies: - axios: ^0.x || ^1.0.0 - zod: ^3.x - JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -1539,36 +1430,32 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: - ajv: ^8.5.0 + ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-escapes@5.0.0: resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} engines: {node: '>=12'} @@ -1577,14 +1464,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1593,34 +1476,13 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - app-module-path@2.2.0: - resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} - aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - are-we-there-yet@3.0.1: - resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1628,23 +1490,19 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-differ@3.0.0: - resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} - engines: {node: '>=8'} - array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} engines: {node: '>= 0.4'} array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -1663,28 +1521,16 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - arrify@2.0.1: - resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} - engines: {node: '>=8'} - - assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-module-types@5.0.0: - resolution: {integrity: sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ==} - engines: {node: '>=14'} - - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} + ast-v8-to-istanbul@1.0.5: + resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + 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==} @@ -1693,124 +1539,73 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.7.9: - resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} - - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.1.0: - resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} - peerDependencies: - '@babel/core': ^7.0.0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + bin-links@5.0.0: + resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} + engines: {node: ^18.17.0 || >=20.5.0} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - builtins@1.0.3: - resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} - - builtins@5.1.0: - resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - byte-size@8.1.1: resolution: {integrity: sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==} engines: {node: '>=12.17'} - c8@7.14.0: - resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} - engines: {node: '>=10.12.0'} - hasBin: true - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - cacache@16.1.3: - resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - cacache@17.1.4: - resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} - - cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} - call-bind-apply-helpers@1.0.1: - resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1823,20 +1618,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001695: - resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} - - chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} chalk@4.1.0: resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==} @@ -1850,34 +1634,28 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - change-case@5.4.4: - resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chalk@6.0.0: + resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==} + engines: {node: '>=22'} - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - cjs-module-lexer@1.4.1: - resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} @@ -1899,18 +1677,14 @@ packages: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} cli-truncate@3.1.0: resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1922,39 +1696,22 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - cmd-shim@6.0.1: - resolution: {integrity: sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q==} + cmd-shim@6.0.3: + resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - code-error-fragment@0.0.230: - resolution: {integrity: sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==} - engines: {node: '>= 4'} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + cmd-shim@7.0.0: + resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} + engines: {node: ^18.17.0 || >=20.5.0} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -1962,6 +1719,9 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1973,10 +1733,6 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - commander@11.0.0: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} @@ -1985,20 +1741,8 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -2010,9 +1754,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -2023,6 +1764,7 @@ packages: conventional-changelog-core@5.0.1: resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. conventional-changelog-preset-loader@3.0.0: resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} @@ -2050,18 +1792,11 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - convict@6.2.4: - resolution: {integrity: sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ==} - engines: {node: '>=6'} - - core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2069,21 +1804,15 @@ packages: typescript: optional: true - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dargs@7.0.0: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} @@ -2120,8 +1849,8 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2137,13 +1866,6 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -2152,28 +1874,12 @@ packages: babel-plugin-macros: optional: true - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2190,70 +1896,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - - dependency-tree@10.0.9: - resolution: {integrity: sha512-dwc59FRIsht+HfnTVM0BCjJaEWxdq2YAvEDy4/Hn6CwS3CBWMtFnL3aZGAkQn3XCYxk/YcTDE4jX2Q7bFTwCjA==} - engines: {node: '>=14'} - hasBin: true - deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} - detect-indent@5.0.0: - resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} - engines: {node: '>=4'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - detective-amd@5.0.2: - resolution: {integrity: sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA==} - engines: {node: '>=14'} - hasBin: true - - detective-cjs@5.0.1: - resolution: {integrity: sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ==} - engines: {node: '>=14'} - - detective-es6@4.0.1: - resolution: {integrity: sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw==} - engines: {node: '>=14'} - - detective-postcss@6.1.3: - resolution: {integrity: sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - detective-sass@5.0.3: - resolution: {integrity: sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA==} - engines: {node: '>=14'} - - detective-scss@4.0.3: - resolution: {integrity: sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg==} - engines: {node: '>=14'} - - detective-stylus@4.0.0: - resolution: {integrity: sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ==} - engines: {node: '>=14'} - - detective-typescript@11.2.0: - resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==} - engines: {node: ^14.14.0 || >=16.0.0} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2270,39 +1915,26 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} - dotenv-expand@10.0.0: - resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} - dotenv@16.3.2: - resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} + ejs@5.0.1: + resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} + engines: {node: '>=0.12.18'} hasBin: true - electron-to-chromium@1.5.83: - resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2312,12 +1944,8 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - enhanced-resolve@5.18.0: - resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} - engines: {node: '>=10.13.0'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -2327,19 +1955,23 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.8.1: - resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} + envinfo@7.13.0: + resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} engines: {node: '>=4'} hasBin: true err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + 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: @@ -2350,26 +1982,31 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.6.0: - resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + 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.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -2381,30 +2018,21 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - eslint-import-resolver-typescript@3.7.0: - resolution: {integrity: sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==} + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -2416,8 +2044,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2437,8 +2065,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-import@2.31.0: - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2447,13 +2075,13 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-prettier@5.2.3: - resolution: {integrity: sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==} + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' eslint: '>=8.0.0' - eslint-config-prettier: '*' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' prettier: '>=3.0.0' peerDependenciesMeta: '@types/eslint': @@ -2479,13 +2107,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -2503,49 +2126,26 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eval-estree-expression@2.0.3: - resolution: {integrity: sha512-6zXgUV+NHvx6PwHxPsIQ8T4cCUgsnhaH6ZyYF1OSKZIrkcAzvSvZgHAbdj72GlNm8eH6c8FI8ywcwqm42Xq1aQ==} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} execa@5.0.0: resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} engines: {node: '>=10'} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - execa@7.2.0: resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - exponential-backoff@3.1.1: - resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} - - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - - extsprintf@1.4.1: - resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} - engines: {'0': node >=0.6.0} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2563,14 +2163,29 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + 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-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} - fastq@1.18.0: - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} @@ -2580,14 +2195,6 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - - filing-cabinet@4.2.0: - resolution: {integrity: sha512-YZ21ryzRcyqxpyKggdYSoXx//d3sCJzM3lsYoaeg/FyXdADGJrUl+BW1KIglaVLJN5BBcMtWylkygY8zBp2MrQ==} - engines: {node: '>=14'} - hasBin: true - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2612,11 +2219,11 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.3.2: - resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} - follow-redirects@1.15.6: - resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2624,44 +2231,25 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - - foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - - form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} engines: {node: '>=14.14'} - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2677,54 +2265,34 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + 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==} - gauge@4.0.4: - resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-amd-module-type@5.0.1: - resolution: {integrity: sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw==} - engines: {node: '>=14'} + 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.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} - get-intrinsic@1.2.7: - resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - get-pkg-repo@4.2.1: resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} engines: {node: '>=6.9.0'} hasBin: true - get-port@5.1.1: - resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} - engines: {node: '>=8'} - get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -2741,12 +2309,13 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.9.0: - resolution: {integrity: sha512-52n24W52sIueosRe0XZ8Ex5Yle+WbhfCKnV/gWXpbVR8FXNTfqdKEKUSypKso66VRHTvvcQxL44UTZbJRlCTnw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} git-raw-commits@3.0.0: resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-remote-origin-url@2.0.0: @@ -2756,13 +2325,14 @@ packages: git-semver-tags@5.0.1: resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-up@7.0.0: resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} - git-url-parse@13.1.0: - resolution: {integrity: sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==} + git-url-parse@14.0.0: + resolution: {integrity: sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==} gitconfiglocal@1.0.0: resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} @@ -2775,30 +2345,19 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@7.1.4: - resolution: {integrity: sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==} - deprecated: Glob versions prior to v9 are no longer supported + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported - - glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -2812,37 +2371,18 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} - - gonzales-pe@4.3.0: - resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} - engines: {node: '>=0.6.0'} - hasBin: true - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} - - graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -2854,10 +2394,6 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2880,43 +2416,39 @@ packages: has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - hosted-git-info@3.0.8: - resolution: {integrity: sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==} - engines: {node: '>=10'} - hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - hosted-git-info@6.1.3: - resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2925,39 +2457,36 @@ packages: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} hasBin: true - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore-walk@5.0.1: - resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - ignore-walk@6.0.5: - resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ignore-walk@8.0.0: + resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} + engines: {node: ^20.17.0 || >=22.9.0} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} import-local@3.1.0: @@ -2965,11 +2494,6 @@ packages: engines: {node: '>=8'} hasBin: true - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2978,9 +2502,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - infer-owner@1.0.4: - resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2991,31 +2512,31 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - init-package-json@5.0.0: - resolution: {integrity: sha512-kBhlSheBfYmq3e0L1ii+VKe3zBTLL5lDCDWR+f9dLmEGSB3MqLlMlsolubSsyI88Bg6EA+BIMlomAnQ1SwgQBw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + init-package-json@8.2.2: + resolution: {integrity: sha512-pXVMn67Jdw2hPKLCuJZj62NC9B2OIDd1R3JwZXTHXuEnfN3Uq5kJbKOSld6YEU+KOGfMD82EzxFTYz5o0SSJoA==} + engines: {node: ^20.17.0 || >=22.9.0} - inquirer@12.3.2: - resolution: {integrity: sha512-YjQCIcDd3yyDuQrbII0FBtm/ZqNoWtvaC71yeCnd5Vbg4EgzsAGaemzfpzmqfvIZEp2roSwuZZKdM0C65hA43g==} + inquirer@12.9.6: + resolution: {integrity: sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' - - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} + peerDependenciesMeta: + '@types/node': + optional: true internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + ip-address@10.7.2: + resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} engines: {node: '>= 12'} - ip@2.0.1: - resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3023,20 +2544,20 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-async-function@2.1.0: - resolution: {integrity: sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==} + 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.1: - resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-bun-module@1.3.0: - resolution: {integrity: sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} @@ -3046,8 +2567,8 @@ packages: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} hasBin: true - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -3063,6 +2584,10 @@ packages: engines: {node: '>=8'} 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'} @@ -3079,12 +2604,8 @@ packages: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -3099,13 +2620,14 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-lambda@1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - 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'} @@ -3114,10 +2636,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -3130,25 +2648,10 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - - is-relative-path@1.0.2: - resolution: {integrity: sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA==} - is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -3157,12 +2660,8 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-ssh@1.4.0: - resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} - - is-stream@2.0.0: - resolution: {integrity: sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==} - engines: {node: '>=8'} + is-ssh@1.4.1: + resolution: {integrity: sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==} is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} @@ -3192,235 +2691,71 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-url-superb@4.0.0: - resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} - engines: {node: '>=10'} - - is-url@1.2.4: - resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - is-weakref@1.1.0: - resolution: {integrity: sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} - engines: {node: '>=8'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jake@10.9.2: - resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} - engines: {node: '>=10'} - hasBin: true - - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.4.1: + resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} - jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true json-buffer@3.0.1: @@ -3432,22 +2767,13 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-parse-even-better-errors@3.0.2: - resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - json-schema-diff@0.18.1: - resolution: {integrity: sha512-lLP/kbwXN85yKWwBGtraxVrJnK/v31D7UZIkSg68BrO+Qeai+aeW1u9b5H1h9uF/Uzzsa8PeaQPTYRaUcdAgWQ==} - engines: {node: '>=v10.24.1'} - hasBin: true - - json-schema-ref-parser@9.0.9: - resolution: {integrity: sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==} - engines: {node: '>=10'} - deprecated: Please switch to @apidevtools/json-schema-ref-parser + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} - json-schema-spec-types@0.1.2: - resolution: {integrity: sha512-MDl8fA8ONckmQOm2+eXKJaFJNvxk7eGin+XFofNjS3q3PRKSoEvgMVb0ehOpCAYkUiLoMiqdU7obV7AmzAmyLw==} + json-parse-even-better-errors@5.0.0: + resolution: {integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==} + engines: {node: ^20.17.0 || >=22.9.0} json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3455,16 +2781,18 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-nice@1.1.4: + resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json-to-ast@2.1.0: - resolution: {integrity: sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==} - engines: {node: '>= 4'} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3477,19 +2805,18 @@ packages: jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsonpointer@5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} + just-diff-apply@5.5.0: + resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} + + just-diff@6.0.2: + resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3498,30 +2825,22 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - lerna@7.4.2: - resolution: {integrity: sha512-gxavfzHfJ4JL30OvMunmlm4Anw7d7Tq6tdVHzUukLdS9nWnxCN/QB21qR+VJYp5tcyXogHKbdUEGh6qmeyzxSA==} - engines: {node: '>=16.0.0'} + lerna@9.0.7: + resolution: {integrity: sha512-PMjbSWYfwL1yZ5c1D2PZuFyzmtYhLdn0f76uG8L25g6eYy34j+2jPb4Q6USx1UJvxVtxkdVEeAAWS/WxgJ8VZA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libnpmaccess@7.0.2: - resolution: {integrity: sha512-vHBVMw1JFMTgEk15zRsJuSAg7QtGGHpUSEfnbcRL1/gTBag9iEfJbyjpDmdJmwMhvpoLoNBtdAUCdGnaP32hhw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + libnpmaccess@10.0.3: + resolution: {integrity: sha512-JPHTfWJxIK+NVPdNMNGnkz4XGX56iijPbe0qFWbdt68HL+kIvSzh+euBL8npLZvl2fpaxo+1eZSdoG15f5YdIQ==} + engines: {node: ^20.17.0 || >=22.9.0} - libnpmpublish@7.3.0: - resolution: {integrity: sha512-fHUxw5VJhZCNSls0KLNEG0mCD2PN1i14gH5elGOgiVnU3VgTcRahagYP2LKI1m0tFCJ+XrAm0zVYyF5RCbXzcg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + libnpmpublish@11.1.2: + resolution: {integrity: sha512-tNcU3cLH7toloAzhOOrBDhjzgbxpyuYvkf+BPPnnJCdc5EIcdJ8JcT+SglvCQKKyZ6m9dVXtCVlJcA6csxKdEA==} + engines: {node: ^20.17.0 || >=22.9.0} lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} @@ -3530,8 +2849,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} lint-staged@14.0.1: @@ -3568,103 +2887,53 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} log-update@5.0.1: resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - - lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} - lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - madge@7.0.0: - resolution: {integrity: sha512-x9eHkBWoCJ2B8yGesWf8LRucarkbH5P3lazqgvmxe4xn5U2Meyfu906iG9mBB1RnY/f4D+gtELWdiz1k6+jAZA==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - typescript: ^3.9.5 || ^4.9.5 || ^5 - peerDependenciesMeta: - typescript: - optional: true - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} - - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + make-fetch-happen@15.0.2: + resolution: {integrity: sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==} + engines: {node: ^20.17.0 || >=22.9.0} - make-fetch-happen@10.2.1: - resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - make-fetch-happen@11.1.1: - resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -3689,10 +2958,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3717,38 +2982,30 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@3.0.5: - resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} + minimatch@3.1.4: + resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} - minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.7: + resolution: {integrity: sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==} engines: {node: '>=16 || 14 >=14.17'} minimist-options@4.1.0: @@ -3758,25 +3015,22 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass-collect@1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} - minipass-fetch@2.1.2: - resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + minipass-fetch@4.0.1: + resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==} + engines: {node: ^18.17.0 || >=20.5.0} - minipass-fetch@3.0.5: - resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} - minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} engines: {node: '>= 8'} - minipass-json-stream@1.0.2: - resolution: {integrity: sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==} - minipass-pipeline@1.2.4: resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} engines: {node: '>=8'} @@ -3785,122 +3039,77 @@ packages: resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} engines: {node: '>=8'} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} engines: {node: '>=8'} - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} modify-values@1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} - module-definition@5.0.1: - resolution: {integrity: sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA==} - engines: {node: '>=14'} - hasBin: true - - module-lookup-amd@8.0.5: - resolution: {integrity: sha512-vc3rYLjDo5Frjox8NZpiyLXsNWJ5BWshztc/5KSOMzpg9k5cHH652YsJ7VKKmtM4SvaxuE9RkrYGhiSjH3Ehow==} - engines: {node: '>=14'} - hasBin: true - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multimatch@5.0.0: - resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} - engines: {node: '>=10'} - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - - mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.4: - resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-addon-api@3.2.1: - resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - - node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true - node-gyp@9.4.1: - resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} - engines: {node: ^12.13 || ^14.13 || >=16} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} hasBin: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-machine-id@1.1.12: - resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} - - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - - node-source-walk@6.0.2: - resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==} - engines: {node: '>=14'} - - nopt@6.0.0: - resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true normalize-package-data@2.5.0: @@ -3910,60 +3119,45 @@ packages: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} engines: {node: '>=10'} - normalize-package-data@5.0.0: - resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-url@8.0.1: - resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} - engines: {node: '>=14.16'} - - npm-bundled@1.1.2: - resolution: {integrity: sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==} - - npm-bundled@3.0.1: - resolution: {integrity: sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-bundled@4.0.0: + resolution: {integrity: sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==} + engines: {node: ^18.17.0 || >=20.5.0} - npm-install-checks@6.3.0: - resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-bundled@5.0.0: + resolution: {integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-normalize-package-bin@1.0.1: - resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==} + npm-install-checks@7.1.2: + resolution: {integrity: sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==} + engines: {node: ^18.17.0 || >=20.5.0} - npm-normalize-package-bin@3.0.1: - resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-install-checks@8.0.0: + resolution: {integrity: sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-package-arg@10.1.0: - resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} - npm-package-arg@8.1.1: - resolution: {integrity: sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg==} - engines: {node: '>=10'} + npm-normalize-package-bin@5.0.0: + resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-packlist@5.1.1: - resolution: {integrity: sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - hasBin: true + npm-package-arg@13.0.1: + resolution: {integrity: sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-packlist@7.0.4: - resolution: {integrity: sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-packlist@10.0.3: + resolution: {integrity: sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-pick-manifest@8.0.2: - resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-pick-manifest@11.0.3: + resolution: {integrity: sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==} + engines: {node: ^20.17.0 || >=22.9.0} - npm-registry-fetch@14.0.5: - resolution: {integrity: sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-registry-fetch@19.1.0: + resolution: {integrity: sha512-xyZLfs7TxPu/WKjHUs0jZOPinzBAI32kEUel6za0vH+JUTnFZ5zbHI1ZoGZRDm6oMjADtrli6FxtMlk/5ABPNw==} + engines: {node: ^20.17.0 || >=22.9.0} npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} @@ -3973,25 +3167,20 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npmlog@6.0.2: - resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - - nx@16.10.0: - resolution: {integrity: sha512-gZl4iCC0Hx0Qe1VWmO4Bkeul2nttuXdPpfnlcDKSACGu3ZIo+uySqwOF8yBAxSTIf8xe2JRhgzJN1aFkuezEBg==} + nx@22.7.10: + resolution: {integrity: sha512-T22WdfbkRi9cLH8a+FkKsNrt540DpNkzkFv1StSsphfu1povyqoVQw9Rpl/poNvnAICNKPwZcpGsy/QXMYkECw==} hasBin: true peerDependencies: - '@swc-node/register': ^1.6.7 - '@swc/core': ^1.3.85 + '@swc-node/register': ^1.11.1 + '@swc/core': ^1.15.8 peerDependenciesMeta: '@swc-node/register': optional: true '@swc/core': optional: true - object-inspect@1.13.3: - resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} object-keys@1.1.1: @@ -4002,6 +3191,10 @@ packages: 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'} @@ -4014,6 +3207,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4033,51 +3230,22 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openapi-diff@0.23.7: - resolution: {integrity: sha512-ABXJ7gTYyawmEC4Z3MiLu7CoD2fLSVfid+ttP9yiuDpytC2PDsc26OTNhYUV9y/z2/ama4CKbszkq3LMd80R6Q==} - engines: {node: '>=6.11.4'} - hasBin: true - - openapi-types@12.1.3: - resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - - openapi-zod-client@1.18.2: - resolution: {integrity: sha512-mfqBxwnGbnfK1CwQb6TBmu8CqVUlHD013Aw82JhDf0iGZsd5oemlPzO8QtteLAaAE6cmLNmSG/tQeBjQV0vB9g==} - hasBin: true - - openapi3-ts@2.0.2: - resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==} - - openapi3-ts@3.1.0: - resolution: {integrity: sha512-1qKTvCCVoV0rkwUh1zq5o8QyghmwYPuhdvtjv1rFjuOnJToXhQyF8eGjNETQ8QmGjr9Jz/tkAKLITIl2s7dw3A==} - - openapi3-ts@4.4.0: - resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} engines: {node: '>=10'} - ora@8.1.1: - resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} - engines: {node: '>=18'} - - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} engines: {node: '>= 0.4'} - p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -4114,6 +3282,10 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-map@7.0.6: + resolution: {integrity: sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==} + engines: {node: '>=18'} + p-pipe@3.1.0: resolution: {integrity: sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw==} engines: {node: '>=8'} @@ -4145,23 +3317,18 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-json@8.1.1: - resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} - engines: {node: '>=14.16'} - - pacote@15.2.0: - resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + pacote@21.5.1: + resolution: {integrity: sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-github-url@1.0.3: - resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} - engines: {node: '>= 0.10'} - hasBin: true + parse-conflict-json@4.0.0: + resolution: {integrity: sha512-37CN2VtcuvKgHUs8+0b1uJeEsbGn61GRHz469C94P5xiOoqpDYJYwjg4RY9Vmz39WyZAVkR5++nbJwLMIgOCnQ==} + engines: {node: ^18.17.0 || >=20.5.0} parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} @@ -4171,28 +3338,12 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-ms@2.1.0: - resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} - engines: {node: '>=6'} - - parse-path@7.0.0: - resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} + parse-path@7.1.0: + resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} parse-url@8.1.0: resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} - pastable@2.2.1: - resolution: {integrity: sha512-K4ClMxRKpgN4sXj6VIPPrvor/TMp2yPNCGtfhvV106C73SwefQ3FuegURsH7AQHpqu0WwbvKXRl1HQxF6qax9w==} - engines: {node: '>=14.x'} - peerDependencies: - react: '>=17' - xstate: '>=4.32.1' - peerDependenciesMeta: - react: - optional: true - xstate: - optional: true - path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -4216,9 +3367,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -4228,24 +3379,20 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - - pathe@2.0.2: - resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} - - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -4259,18 +3406,6 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pify@5.0.0: - resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} - engines: {node: '>=10'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -4279,102 +3414,75 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-values-parser@6.0.2: - resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} - engines: {node: '>=10'} - peerDependencies: - postcss: ^8.2.9 + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} + engines: {node: '>=4'} - postcss@8.5.1: - resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} + postcss@8.5.24: + resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} engines: {node: ^10 || ^12 || >=14} - precinct@11.0.5: - resolution: {integrity: sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w==} - engines: {node: ^14.14.0 || >=16.0.0} - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} - engines: {node: '>=14'} - hasBin: true - - prettier@3.4.2: - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} hasBin: true - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.4.1: + resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - pretty-ms@7.0.1: - resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} - engines: {node: '>=10'} + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} - proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true + proggy@3.0.0: + resolution: {integrity: sha512-QE8RApCM3IaRRxVzxrjbgNMpQEX6Wu0p0KBeoSiSEw5/bsGwZHsshF4LCxH2jp/r6BU+bqA3LrMDEYNfJnpD8Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + promise-all-reject-late@1.0.1: + resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} + + promise-call-limit@3.0.2: + resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - promzard@1.0.2: - resolution: {integrity: sha512-2FPputGL+mP3jJ3UZg/Dl9YOkovB7DX0oOr+ck5QbZ5MtORtds8k/BZdn+02peDLI8/YWbmzx34k5fA+fHvCVQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - - protocols@2.0.1: - resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} + promzard@2.0.0: + resolution: {integrity: sha512-Ncd0vyS2eXGOjchIRg6PVCYKetJYrW1BSbbIo+bKdig61TB6nH2RQNF2uP+qMpsI73L/jURLWojcw8JNIKZ3gg==} + engines: {node: ^18.17.0 || >=20.5.0} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + protocols@2.0.2: + resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} - pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + 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'} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4382,32 +3490,19 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - - quote-unquote@1.0.0: - resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + read-cmd-shim@4.0.0: resolution: {integrity: sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - read-package-json-fast@3.0.2: - resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - read-package-json@6.0.4: - resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - deprecated: This package is no longer supported. Please use @npmcli/package-json instead. + read-cmd-shim@5.0.0: + resolution: {integrity: sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==} + engines: {node: ^18.17.0 || >=20.5.0} read-pkg-up@3.0.0: resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} @@ -4425,13 +3520,9 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} - read@2.1.0: - resolution: {integrity: sha512-bvxi1QLJHcaywCAEsAk4DG3nVoqiY2Csps3qzWalhj5hFqRn1d/OixkFXtLO1PrgHUcAP0FNaSY/5GYNfENFFQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - read@3.0.1: - resolution: {integrity: sha512-SLBrDU/Srs/9EoWhU5GdbAoxG1GzpQHo/6qiGItaoLJ1thmYpcNIM1qISEUvyHBzfGlWIyd6p2DNi1oV1VmAuw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + read@4.1.0: + resolution: {integrity: sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA==} + engines: {node: ^18.17.0 || >=20.5.0} readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -4448,21 +3539,10 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - registry-auth-token@5.0.3: - resolution: {integrity: sha512-1bpc9IyC+e+CNFRaWyn77tk4xGG4PPUyfakSmA6F6cvUDjrm58dfyJ3II+9yb10EDkHoy1LaPSmHaWLOH3m6HA==} - engines: {node: '>=14'} - - registry-url@6.0.1: - resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} - engines: {node: '>=12'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4471,26 +3551,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - requirejs-config-file@4.0.0: - resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} - engines: {node: '>=10.13.0'} - - requirejs@2.3.7: - resolution: {integrity: sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==} - engines: {node: '>=0.4.0'} - hasBin: true - - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} - resolve-dependency-path@3.0.2: - resolution: {integrity: sha512-Tz7zfjhLfsvR39ADOSk9us4421J/1ztVBo4rWUkF38hgHK5m0OCZ3NxFVpqHRkjctnwVa15igEUHFJp8MCS7vA==} - engines: {node: '>=14'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4506,14 +3570,15 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true - responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -4531,8 +3596,8 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rfdc@1.4.1: @@ -4543,36 +3608,28 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@4.4.1: - resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} - engines: {node: '>=14'} - hasBin: true - - rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} hasBin: true - rollup@4.31.0: - resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} + rollup@4.62.3: + resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - - run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} engines: {node: '>=0.12.0'} run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.1.2: @@ -4592,14 +3649,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass-lookup@5.0.1: - resolution: {integrity: sha512-t0X5PaizPc2H4+rCwszAqHZRtr4bugo4pgiCvrBFvIX0XFxnr29g77LJcpyj9A0DcKf7gXMLcgvRjsonYI6x4g==} - engines: {node: '>=14'} - hasBin: true - - sembear@0.5.2: - resolution: {integrity: sha512-Ij1vCAdFgWABd7zTg50Xw1/p0JgESNxuLlneEAsmBrKishA06ulTTL/SHGmNy2Zud7+rKrHTKNI6moJsn1ppAQ==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4608,18 +3657,20 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + 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==} @@ -4633,28 +3684,16 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -4665,8 +3704,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -4679,26 +3718,14 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sigstore@1.9.0: - resolution: {integrity: sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sigstore@4.1.1: + resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} + engines: {node: ^20.17.0 || >=22.9.0} slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -4707,32 +3734,26 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - socks-proxy-agent@7.0.0: - resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} - engines: {node: '>= 10'} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} + engines: {node: '>= 18'} - socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} - sort-keys@2.0.0: - resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} - engines: {node: '>=4'} + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - spawndamnit@2.0.0: - resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} - spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -4742,8 +3763,8 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - spdx-license-ids@3.0.21: - resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} split2@3.2.2: resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} @@ -4751,48 +3772,35 @@ packages: split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - - ssri@10.0.6: - resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - ssri@9.0.1: - resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + ssri@12.0.0: + resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} + engines: {node: ^18.17.0 || >=20.5.0} - stable-hash@0.0.4: - resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} engines: {node: '>=18'} - stream-to-array@2.3.0: - resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4801,16 +3809,16 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} + engines: {node: '>=20'} - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} engines: {node: '>= 0.4'} - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: @@ -4823,16 +3831,12 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -4855,87 +3859,28 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strong-log-transformer@2.1.0: - resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==} - engines: {node: '>=4'} - hasBin: true - - stylus-lookup@5.0.1: - resolution: {integrity: sha512-tLtJEd5AGvnVy4f9UHQMw4bkJJtaAcmo54N+ovQBjDY3DuWyK9Eltxzr5+KG0q4ew6v2EHyuWWNnHeiw/Eo7rQ==} - engines: {node: '>=14'} - hasBin: true - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-parser@10.0.3: - resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} - engines: {node: '>=10'} - - synckit@0.9.2: - resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} - table@6.9.0: - resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} - engines: {node: '>=10.0.0'} - - tanu@0.1.13: - resolution: {integrity: sha512-UbRmX7ccZ4wMVOY/Uw+7ji4VOkEYSYJG1+I4qzbnn4qh/jtvVbrm6BFnF12NQQ4+jGv21wKmjb1iFyUSVnBWcQ==} - - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@6.1.11: - resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} - engines: {node: '>= 10'} - - tar@6.2.0: - resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} - engines: {node: '>=10'} - - temp-dir@1.0.0: - resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} - engines: {node: '>=4'} - - temporal-polyfill@0.2.5: - resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==} - - temporal-spec@0.2.4: - resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} - - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - - test-exclude@7.0.1: - resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + tar@7.5.21: + resolution: {integrity: sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==} engines: {node: '>=18'} text-extensions@1.9.0: @@ -4954,38 +3899,37 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} - engines: {node: ^18.0.0 || >=20.0.0} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.2.3: - resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + treeverse@3.0.0: + resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} @@ -4997,54 +3941,6 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-graphviz@1.8.2: - resolution: {integrity: sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==} - engines: {node: '>=14.16'} - - ts-jest@29.2.5: - resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - - ts-pattern@5.6.2: - resolution: {integrity: sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==} - - ts-toolbelt@9.6.0: - resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} - tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -5052,30 +3948,17 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - - tuf-js@1.1.7: - resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + tuf-js@4.1.0: + resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} + engines: {node: ^20.17.0 || >=22.9.0} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -5084,14 +3967,6 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@0.4.1: - resolution: {integrity: sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==} - engines: {node: '>=6'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -5104,10 +3979,6 @@ packages: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} - type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5116,22 +3987,17 @@ packages: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -5146,49 +4012,29 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - - unique-filename@2.0.1: - resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unique-filename@3.0.0: - resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - unique-slug@3.0.0: - resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - unique-slug@4.0.0: - resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + undici@6.29.0: + resolution: {integrity: sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==} + engines: {node: '>=18.17'} universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} - update-browserslist-db@1.1.2: - resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -5196,59 +4042,29 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - v8-compile-cache@2.3.0: - resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validate-npm-package-name@3.0.0: - resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} - - validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} - - verror@1.10.1: - resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} - engines: {node: '>=0.6.0'} - - vite-node@3.0.3: - resolution: {integrity: sha512-0sQcwhwAEw/UJGojbhOrnq3HtiZ3tC7BzpAa0lx3QaTX0S3YX70iGcik25UBdB96pmdwjyY2uyKNYruxCDmiEg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true + validate-npm-package-name@6.0.2: + resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} + engines: {node: ^18.17.0 || >=20.5.0} - vite@6.0.11: - resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 jiti: '>=1.21.0' - less: '*' + less: ^4.0.0 lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + 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 + yaml: ^2.8.3 peerDependenciesMeta: '@types/node': optional: true @@ -5273,23 +4089,39 @@ packages: yaml: optional: true - vitest@3.0.3: - resolution: {integrity: sha512-dWdwTFUW9rcnL0LyF2F+IfvNQWB0w9DERySCk8VMG75F8k25C7LsZoh6XfCjPvcR8Nb+Lqi9JKr6vnzH7HSrpQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.3 - '@vitest/ui': 3.0.3 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' + vite: ^7.3.5 peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@opentelemetry/api': + optional: true '@types/node': optional: true - '@vitest/browser': + '@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 @@ -5298,43 +4130,13 @@ packages: jsdom: optional: true - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - walkdir@0.4.1: - resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} - engines: {node: '>=6.0.0'} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - whence@2.0.1: - resolution: {integrity: sha512-VtcCE1Pe3BKofF/k+P5xcpuoqQ0f1NJY6TmdUw5kInl9/pEr1ZEFD9+ZOUicf52tvpTbhMS93aWXriu2IQYTTw==} - engines: {node: '>=14'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -5347,22 +4149,18 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} engines: {node: '>= 0.4'} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - which@3.0.1: - resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true why-is-node-running@2.3.0: @@ -5395,24 +4193,13 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@2.4.3: - resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - write-json-file@3.2.0: - resolution: {integrity: sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==} - engines: {node: '>=6'} - - write-pkg@4.0.0: - resolution: {integrity: sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA==} - engines: {node: '>=8'} + write-file-atomic@6.0.0: + resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} + engines: {node: ^18.17.0 || >=20.5.0} xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -5422,37 +4209,26 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} - yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - yaml@2.5.1: - resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} - engines: {node: '>= 14'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} hasBin: true - yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} - engines: {node: '>= 14'} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} hasBin: true - yargs-parser@20.2.4: - resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} - engines: {node: '>=10'} - yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -5461,844 +4237,475 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} engines: {node: '>=10'} yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} - z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true - - zod@3.24.1: - resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@apidevtools/json-schema-ref-parser@11.7.2': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.1.0 - - '@apidevtools/json-schema-ref-parser@9.0.9': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - call-me-maybe: 1.0.2 - js-yaml: 4.1.0 - - '@apidevtools/json-schema-ref-parser@9.1.2': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - call-me-maybe: 1.0.2 - js-yaml: 4.1.0 - - '@apidevtools/openapi-schemas@2.1.0': {} - - '@apidevtools/swagger-methods@3.0.2': {} - - '@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3)': - dependencies: - '@apidevtools/json-schema-ref-parser': 9.1.2 - '@apidevtools/openapi-schemas': 2.1.0 - '@apidevtools/swagger-methods': 3.0.2 - '@jsdevtools/ono': 7.1.3 - call-me-maybe: 1.0.2 - openapi-types: 12.1.3 - z-schema: 5.0.5 - - '@apidevtools/swagger-parser@10.1.1(openapi-types@12.1.3)': - dependencies: - '@apidevtools/json-schema-ref-parser': 11.7.2 - '@apidevtools/openapi-schemas': 2.1.0 - '@apidevtools/swagger-methods': 3.0.2 - '@jsdevtools/ono': 7.1.3 - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - call-me-maybe: 1.0.2 - openapi-types: 12.1.3 - - '@babel/code-frame@7.25.9': - dependencies: - '@babel/highlight': 7.25.9 - picocolors: 1.1.1 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.26.5': {} - - '@babel/core@7.26.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.5 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helpers': 7.26.0 - '@babel/parser': 7.26.5 - '@babel/template': 7.25.9 - '@babel/traverse': 7.26.5 - '@babel/types': 7.26.5 - convert-source-map: 2.0.0 - debug: 4.4.0 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.26.5': - dependencies: - '@babel/parser': 7.26.5 - '@babel/types': 7.26.5 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.26.5': - dependencies: - '@babel/compat-data': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-module-imports@7.25.9': - dependencies: - '@babel/traverse': 7.26.5 - '@babel/types': 7.26.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.25.9': {} - - '@babel/helpers@7.26.0': - dependencies: - '@babel/template': 7.25.9 - '@babel/types': 7.26.5 - - '@babel/highlight@7.25.9': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.25.9 - chalk: 2.4.2 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/parser@7.26.5': - dependencies: - '@babel/types': 7.26.5 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-string-parser@7.29.7': {} - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/parser@7.29.8': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/types': 7.29.8 - '@babel/runtime@7.26.0': + '@babel/types@7.29.8': dependencies: - regenerator-runtime: 0.14.1 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 - '@babel/template@7.25.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.5 - '@babel/types': 7.26.5 + '@bcoe/v8-coverage@1.0.2': {} - '@babel/traverse@7.26.5': + '@emnapi/core@1.10.0': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.5 - '@babel/parser': 7.26.5 - '@babel/template': 7.25.9 - '@babel/types': 7.26.5 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 - '@babel/types@7.26.5': + '@emnapi/core@1.4.5': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 - '@bcoe/v8-coverage@0.2.3': {} + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 - '@bcoe/v8-coverage@1.0.2': {} + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 - '@cspotcode/source-map-support@0.8.1': + '@emnapi/wasi-threads@1.0.4': dependencies: - '@jridgewell/trace-mapping': 0.3.9 + tslib: 2.8.1 - '@dependents/detective-less@4.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 6.0.2 + tslib: 2.8.1 - '@esbuild/aix-ppc64@0.24.2': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.24.2': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.24.2': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.24.2': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.24.2': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.24.2': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.24.2': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.24.2': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.24.2': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.24.2': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.24.2': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.24.2': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.24.2': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.24.2': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.24.2': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.24.2': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.24.2': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.24.2': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.24.2': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.24.2': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.24.2': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/sunos-x64@0.24.2': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/win32-arm64@0.24.2': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-ia32@0.24.2': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-x64@0.24.2': + '@esbuild/win32-ia32@0.28.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} '@eslint/eslintrc@2.1.4': dependencies: - ajv: 6.12.6 - debug: 4.4.0 + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color '@eslint/js@8.57.1': {} - '@gar/promisify@1.1.3': {} + '@gar/promise-retry@1.0.3': {} '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 - minimatch: 3.1.2 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 transitivePeerDependencies: - supports-color '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/momoa@2.0.4': {} - '@humanwhocodes/object-schema@2.0.3': {} '@hutson/parse-repository-url@3.0.2': {} - '@inquirer/checkbox@4.0.6(@types/node@22.10.7)': - dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/figures': 1.0.9 - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - - '@inquirer/confirm@5.1.3(@types/node@22.10.7)': - dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - - '@inquirer/core@10.1.4(@types/node@22.10.7)': - dependencies: - '@inquirer/figures': 1.0.9 - '@inquirer/type': 3.0.2(@types/node@22.10.7) - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - transitivePeerDependencies: - - '@types/node' + '@inquirer/ansi@1.0.2': {} - '@inquirer/editor@4.2.3(@types/node@22.10.7)': - dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - external-editor: 3.1.0 + '@inquirer/ansi@2.0.8': {} - '@inquirer/expand@4.0.6(@types/node@22.10.7)': + '@inquirer/checkbox@4.3.2(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - yoctocolors-cjs: 2.1.2 - - '@inquirer/figures@1.0.9': {} + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/input@4.1.3(@types/node@22.10.7)': + '@inquirer/checkbox@5.2.5(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/number@3.0.6(@types/node@22.10.7)': + '@inquirer/confirm@5.1.21(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/password@4.0.6(@types/node@22.10.7)': + '@inquirer/confirm@6.3.2(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/prompts@7.2.3(@types/node@22.10.7)': + '@inquirer/core@10.3.2(@types/node@22.20.4)': dependencies: - '@inquirer/checkbox': 4.0.6(@types/node@22.10.7) - '@inquirer/confirm': 5.1.3(@types/node@22.10.7) - '@inquirer/editor': 4.2.3(@types/node@22.10.7) - '@inquirer/expand': 4.0.6(@types/node@22.10.7) - '@inquirer/input': 4.1.3(@types/node@22.10.7) - '@inquirer/number': 3.0.6(@types/node@22.10.7) - '@inquirer/password': 4.0.6(@types/node@22.10.7) - '@inquirer/rawlist': 4.0.6(@types/node@22.10.7) - '@inquirer/search': 3.0.6(@types/node@22.10.7) - '@inquirer/select': 4.0.6(@types/node@22.10.7) - '@types/node': 22.10.7 + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.4) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/rawlist@4.0.6(@types/node@22.10.7)': + '@inquirer/core@12.0.3(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - yoctocolors-cjs: 2.1.2 + '@inquirer/ansi': 2.0.8 + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@22.20.4) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/search@3.0.6(@types/node@22.10.7)': + '@inquirer/editor@4.2.23(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/figures': 1.0.9 - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/external-editor': 1.0.3(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/select@4.0.6(@types/node@22.10.7)': + '@inquirer/editor@5.3.3(@types/node@22.20.4)': dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/figures': 1.0.9 - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/external-editor': 3.0.5(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@inquirer/type@3.0.2(@types/node@22.10.7)': + '@inquirer/expand@4.0.23(@types/node@22.20.4)': dependencies: - '@types/node': 22.10.7 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@isaacs/cliui@8.0.2': + '@inquirer/expand@5.1.5(@types/node@22.20.4)': dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@istanbuljs/load-nyc-config@1.1.0': + '@inquirer/external-editor@1.0.3(@types/node@22.20.4)': dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.4 - '@jest/console@29.7.0': + '@inquirer/external-editor@3.0.5(@types/node@22.20.4)': dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.4 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node + '@inquirer/figures@1.0.15': {} - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - jest-mock: 29.7.0 + '@inquirer/figures@2.0.9': {} - '@jest/expect-utils@29.7.0': + '@inquirer/input@4.3.1(@types/node@22.20.4)': dependencies: - jest-get-type: 29.6.3 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/expect@29.7.0': + '@inquirer/input@5.1.6(@types/node@22.20.4)': dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/fake-timers@29.7.0': + '@inquirer/number@3.0.23(@types/node@22.20.4)': dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.10.7 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/globals@29.7.0': + '@inquirer/number@4.2.3(@types/node@22.20.4)': dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/reporters@29.7.0': + '@inquirer/password@4.0.23(@types/node@22.20.4)': dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.10.7 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.7 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/schemas@29.6.3': + '@inquirer/password@5.2.2(@types/node@22.20.4)': dependencies: - '@sinclair/typebox': 0.27.8 + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 + + '@inquirer/prompts@7.10.1(@types/node@22.20.4)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.20.4) + '@inquirer/confirm': 5.1.21(@types/node@22.20.4) + '@inquirer/editor': 4.2.23(@types/node@22.20.4) + '@inquirer/expand': 4.0.23(@types/node@22.20.4) + '@inquirer/input': 4.3.1(@types/node@22.20.4) + '@inquirer/number': 3.0.23(@types/node@22.20.4) + '@inquirer/password': 4.0.23(@types/node@22.20.4) + '@inquirer/rawlist': 4.1.11(@types/node@22.20.4) + '@inquirer/search': 3.2.2(@types/node@22.20.4) + '@inquirer/select': 4.4.2(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 + + '@inquirer/prompts@8.7.2(@types/node@22.20.4)': + dependencies: + '@inquirer/checkbox': 5.2.5(@types/node@22.20.4) + '@inquirer/confirm': 6.3.2(@types/node@22.20.4) + '@inquirer/editor': 5.3.3(@types/node@22.20.4) + '@inquirer/expand': 5.1.5(@types/node@22.20.4) + '@inquirer/input': 5.1.6(@types/node@22.20.4) + '@inquirer/number': 4.2.3(@types/node@22.20.4) + '@inquirer/password': 5.2.2(@types/node@22.20.4) + '@inquirer/rawlist': 5.3.5(@types/node@22.20.4) + '@inquirer/search': 4.3.3(@types/node@22.20.4) + '@inquirer/select': 5.2.5(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/source-map@29.6.3': + '@inquirer/rawlist@4.1.11(@types/node@22.20.4)': dependencies: - '@jridgewell/trace-mapping': 0.3.25 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@jest/test-result@29.7.0': + '@inquirer/rawlist@5.3.5(@types/node@22.20.4)': dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/test-sequencer@29.7.0': + '@inquirer/search@3.2.2(@types/node@22.20.4)': dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@jest/transform@29.7.0': + '@inquirer/search@4.3.3(@types/node@22.20.4)': dependencies: - '@babel/core': 7.26.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.6 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jest/types@29.6.3': + '@inquirer/select@4.4.2(@types/node@22.20.4)': dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.10.7 - '@types/yargs': 17.0.33 - chalk: 4.1.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.4) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.4 - '@jridgewell/gen-mapping@0.3.8': + '@inquirer/select@5.2.5(@types/node@22.20.4)': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@22.20.4) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@22.20.4) + optionalDependencies: + '@types/node': 22.20.4 - '@jridgewell/resolve-uri@3.1.2': {} + '@inquirer/type@3.0.10(@types/node@22.20.4)': + optionalDependencies: + '@types/node': 22.20.4 - '@jridgewell/set-array@1.2.1': {} + '@inquirer/type@4.1.1(@types/node@22.20.4)': + optionalDependencies: + '@types/node': 22.20.4 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@isaacs/cliui@9.0.0': {} - '@jridgewell/trace-mapping@0.3.25': + '@isaacs/fs-minipass@4.0.1': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + minipass: 7.1.3 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@isaacs/string-locale-compare@1.1.0': {} - '@jsdevtools/ono@7.1.3': {} + '@jest/diff-sequences@30.0.1': {} - '@lerna/child-process@7.4.2': - dependencies: - chalk: 4.1.2 - execa: 5.0.0 - strong-log-transformer: 2.1.0 + '@jest/diff-sequences@30.4.0': {} - '@lerna/create@7.4.2(encoding@0.1.13)(typescript@5.7.3)': - dependencies: - '@lerna/child-process': 7.4.2 - '@npmcli/run-script': 6.0.2 - '@nx/devkit': 16.10.0(nx@16.10.0) - '@octokit/plugin-enterprise-rest': 6.0.1 - '@octokit/rest': 19.0.11(encoding@0.1.13) - byte-size: 8.1.1 - chalk: 4.1.0 - clone-deep: 4.0.1 - cmd-shim: 6.0.1 - columnify: 1.6.0 - conventional-changelog-core: 5.0.1 - conventional-recommended-bump: 7.0.1 - cosmiconfig: 8.3.6(typescript@5.7.3) - dedent: 0.7.0 - execa: 5.0.0 - fs-extra: 11.3.0 - get-stream: 6.0.0 - git-url-parse: 13.1.0 - glob-parent: 5.1.2 - globby: 11.1.0 - graceful-fs: 4.2.11 - has-unicode: 2.0.1 - ini: 1.3.8 - init-package-json: 5.0.0 - inquirer: 8.2.6 - is-ci: 3.0.1 - is-stream: 2.0.0 - js-yaml: 4.1.0 - libnpmpublish: 7.3.0 - load-json-file: 6.2.0 - lodash: 4.17.21 - make-dir: 4.0.0 - minimatch: 3.0.5 - multimatch: 5.0.0 - node-fetch: 2.6.7(encoding@0.1.13) - npm-package-arg: 8.1.1 - npm-packlist: 5.1.1 - npm-registry-fetch: 14.0.5 - npmlog: 6.0.2 - nx: 16.10.0 - p-map: 4.0.0 - p-map-series: 2.1.0 - p-queue: 6.6.2 - p-reduce: 2.1.0 - pacote: 15.2.0 - pify: 5.0.0 - read-cmd-shim: 4.0.0 - read-package-json: 6.0.4 - resolve-from: 5.0.0 - rimraf: 4.4.1 - semver: 7.6.3 - signal-exit: 3.0.7 - slash: 3.0.0 - ssri: 9.0.1 - strong-log-transformer: 2.1.0 - tar: 6.1.11 - temp-dir: 1.0.0 - upath: 2.0.1 - uuid: 9.0.1 - validate-npm-package-license: 3.0.4 - validate-npm-package-name: 5.0.0 - write-file-atomic: 5.0.1 - write-pkg: 4.0.0 - yargs: 16.2.0 - yargs-parser: 20.2.4 - transitivePeerDependencies: - - '@swc-node/register' - - '@swc/core' - - bluebird - - debug - - encoding - - supports-color - - typescript + '@jest/get-type@30.1.0': {} - '@liuli-util/fs-extra@0.1.0': + '@jest/schemas@30.4.1': dependencies: - '@types/fs-extra': 9.0.13 - fs-extra: 10.1.0 + '@sinclair/typebox': 0.34.52 - '@manypkg/cli@0.21.4': - dependencies: - '@manypkg/get-packages': 2.2.2 - chalk: 2.4.2 - detect-indent: 6.1.0 - find-up: 4.1.0 - fs-extra: 8.1.0 - normalize-path: 3.0.0 - p-limit: 2.3.0 - package-json: 8.1.1 - parse-github-url: 1.0.3 - sembear: 0.5.2 - semver: 6.3.1 - spawndamnit: 2.0.0 - validate-npm-package-name: 3.0.0 + '@jridgewell/resolve-uri@3.1.2': {} - '@manypkg/find-root@2.2.3': + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': dependencies: - '@manypkg/tools': 1.1.2 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 - '@manypkg/get-packages@2.2.2': + '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@manypkg/find-root': 2.2.3 - '@manypkg/tools': 1.1.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.9.0 - '@manypkg/tools@1.1.2': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - fast-glob: 3.3.3 - jju: 1.4.0 - js-yaml: 4.1.0 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true '@nodelib/fs.scandir@2.1.5': dependencies: @@ -6310,455 +4717,440 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.18.0 + fastq: 1.20.1 '@nolyfill/is-core-module@1.0.39': {} - '@npmcli/fs@2.1.2': + '@npmcli/agent@4.0.2': dependencies: - '@gar/promisify': 1.1.3 - semver: 7.6.3 + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.5.2 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color - '@npmcli/fs@3.1.1': + '@npmcli/arborist@9.1.6': + dependencies: + '@isaacs/string-locale-compare': 1.1.0 + '@npmcli/fs': 4.0.0 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/map-workspaces': 5.0.3 + '@npmcli/metavuln-calculator': 9.0.3 + '@npmcli/name-from-folder': 3.0.0 + '@npmcli/node-gyp': 4.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/query': 4.0.1 + '@npmcli/redact': 3.2.2 + '@npmcli/run-script': 10.0.3 + bin-links: 5.0.0 + cacache: 20.0.4 + common-ancestor-path: 1.0.1 + hosted-git-info: 9.0.3 + json-stringify-nice: 1.1.4 + lru-cache: 11.5.2 + minimatch: 10.2.6 + nopt: 8.1.0 + npm-install-checks: 7.1.2 + npm-package-arg: 13.0.1 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.0 + pacote: 21.5.1 + parse-conflict-json: 4.0.0 + proc-log: 5.0.0 + proggy: 3.0.0 + promise-all-reject-late: 1.0.1 + promise-call-limit: 3.0.2 + semver: 7.7.2 + ssri: 12.0.0 + treeverse: 3.0.0 + walk-up-path: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@4.0.0': dependencies: - semver: 7.6.3 + semver: 7.7.2 - '@npmcli/git@4.1.0': + '@npmcli/fs@5.0.0': dependencies: - '@npmcli/promise-spawn': 6.0.2 - lru-cache: 7.18.3 - npm-pick-manifest: 8.0.2 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.6.3 - which: 3.0.1 - transitivePeerDependencies: - - bluebird + semver: 7.8.5 - '@npmcli/installed-package-contents@2.1.0': + '@npmcli/git@7.0.2': dependencies: - npm-bundled: 3.0.1 - npm-normalize-package-bin: 3.0.1 + '@gar/promise-retry': 1.0.3 + '@npmcli/promise-spawn': 9.0.1 + ini: 6.0.0 + lru-cache: 11.5.2 + npm-pick-manifest: 11.0.3 + proc-log: 6.1.0 + semver: 7.8.5 + which: 6.0.1 - '@npmcli/move-file@2.0.1': + '@npmcli/installed-package-contents@3.0.0': dependencies: - mkdirp: 1.0.4 - rimraf: 3.0.2 + npm-bundled: 4.0.0 + npm-normalize-package-bin: 4.0.0 - '@npmcli/node-gyp@3.0.0': {} + '@npmcli/installed-package-contents@4.0.0': + dependencies: + npm-bundled: 5.0.0 + npm-normalize-package-bin: 5.0.0 - '@npmcli/promise-spawn@6.0.2': + '@npmcli/map-workspaces@5.0.3': dependencies: - which: 3.0.1 + '@npmcli/name-from-folder': 4.0.0 + '@npmcli/package-json': 7.0.2 + glob: 13.0.6 + minimatch: 10.2.6 - '@npmcli/run-script@6.0.2': + '@npmcli/metavuln-calculator@9.0.3': dependencies: - '@npmcli/node-gyp': 3.0.0 - '@npmcli/promise-spawn': 6.0.2 - node-gyp: 9.4.1 - read-package-json-fast: 3.0.2 - which: 3.0.1 + cacache: 20.0.4 + json-parse-even-better-errors: 5.0.0 + pacote: 21.5.1 + proc-log: 6.1.0 + semver: 7.7.2 transitivePeerDependencies: - - bluebird - supports-color - '@nrwl/devkit@16.10.0(nx@16.10.0)': + '@npmcli/name-from-folder@3.0.0': {} + + '@npmcli/name-from-folder@4.0.0': {} + + '@npmcli/node-gyp@4.0.0': {} + + '@npmcli/node-gyp@5.0.0': {} + + '@npmcli/package-json@7.0.2': dependencies: - '@nx/devkit': 16.10.0(nx@16.10.0) - transitivePeerDependencies: - - nx + '@npmcli/git': 7.0.2 + glob: 11.1.0 + hosted-git-info: 9.0.3 + json-parse-even-better-errors: 5.0.0 + proc-log: 6.1.0 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 - '@nrwl/tao@16.10.0': + '@npmcli/promise-spawn@9.0.1': dependencies: - nx: 16.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@swc-node/register' - - '@swc/core' - - debug + which: 6.0.1 + + '@npmcli/query@4.0.1': + dependencies: + postcss-selector-parser: 7.1.6 - '@nx/devkit@16.10.0(nx@16.10.0)': + '@npmcli/redact@3.2.2': {} + + '@npmcli/redact@4.0.0': {} + + '@npmcli/run-script@10.0.3': + dependencies: + '@npmcli/node-gyp': 5.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/promise-spawn': 9.0.1 + node-gyp: 12.4.0 + proc-log: 6.1.0 + which: 6.0.1 + + '@nx/devkit@22.7.7(nx@22.7.10)': dependencies: - '@nrwl/devkit': 16.10.0(nx@16.10.0) - ejs: 3.1.10 + '@zkochan/js-yaml': 0.0.7 + ejs: 5.0.1 enquirer: 2.3.6 - ignore: 5.3.2 - nx: 16.10.0 - semver: 7.5.3 - tmp: 0.2.3 + minimatch: 10.2.5 + nx: 22.7.10 + semver: 7.7.2 tslib: 2.8.1 + yargs-parser: 21.1.1 - '@nx/nx-darwin-arm64@16.10.0': + '@nx/nx-darwin-arm64@22.7.10': optional: true - '@nx/nx-darwin-x64@16.10.0': + '@nx/nx-darwin-x64@22.7.10': optional: true - '@nx/nx-freebsd-x64@16.10.0': + '@nx/nx-freebsd-x64@22.7.10': optional: true - '@nx/nx-linux-arm-gnueabihf@16.10.0': + '@nx/nx-linux-arm-gnueabihf@22.7.10': optional: true - '@nx/nx-linux-arm64-gnu@16.10.0': + '@nx/nx-linux-arm64-gnu@22.7.10': optional: true - '@nx/nx-linux-arm64-musl@16.10.0': + '@nx/nx-linux-arm64-musl@22.7.10': optional: true - '@nx/nx-linux-x64-gnu@16.10.0': + '@nx/nx-linux-x64-gnu@22.7.10': optional: true - '@nx/nx-linux-x64-musl@16.10.0': + '@nx/nx-linux-x64-musl@22.7.10': optional: true - '@nx/nx-win32-arm64-msvc@16.10.0': + '@nx/nx-win32-arm64-msvc@22.7.10': optional: true - '@nx/nx-win32-x64-msvc@16.10.0': + '@nx/nx-win32-x64-msvc@22.7.10': optional: true - '@octokit/auth-token@3.0.4': {} + '@octokit/auth-token@4.0.0': {} - '@octokit/core@4.2.4(encoding@0.1.13)': + '@octokit/core@5.2.2': dependencies: - '@octokit/auth-token': 3.0.4 - '@octokit/graphql': 5.0.6(encoding@0.1.13) - '@octokit/request': 6.2.8(encoding@0.1.13) - '@octokit/request-error': 3.0.3 - '@octokit/types': 9.3.2 + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.1 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 before-after-hook: 2.2.3 universal-user-agent: 6.0.1 - transitivePeerDependencies: - - encoding - '@octokit/endpoint@7.0.6': + '@octokit/endpoint@9.0.6': dependencies: - '@octokit/types': 9.3.2 - is-plain-object: 5.0.0 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - '@octokit/graphql@5.0.6(encoding@0.1.13)': + '@octokit/graphql@7.1.1': dependencies: - '@octokit/request': 6.2.8(encoding@0.1.13) - '@octokit/types': 9.3.2 + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - transitivePeerDependencies: - - encoding - '@octokit/openapi-types@18.1.1': {} + '@octokit/openapi-types@24.2.0': {} '@octokit/plugin-enterprise-rest@6.0.1': {} - '@octokit/plugin-paginate-rest@6.1.2(@octokit/core@4.2.4(encoding@0.1.13))': + '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 4.2.4(encoding@0.1.13) - '@octokit/tsconfig': 1.0.2 - '@octokit/types': 9.3.2 + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 - '@octokit/plugin-request-log@1.0.4(@octokit/core@4.2.4(encoding@0.1.13))': + '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 4.2.4(encoding@0.1.13) + '@octokit/core': 5.2.2 - '@octokit/plugin-rest-endpoint-methods@7.2.3(@octokit/core@4.2.4(encoding@0.1.13))': + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 4.2.4(encoding@0.1.13) - '@octokit/types': 10.0.0 + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 - '@octokit/request-error@3.0.3': + '@octokit/request-error@5.1.1': dependencies: - '@octokit/types': 9.3.2 + '@octokit/types': 13.10.0 deprecation: 2.3.1 once: 1.4.0 - '@octokit/request@6.2.8(encoding@0.1.13)': + '@octokit/request@8.4.1': dependencies: - '@octokit/endpoint': 7.0.6 - '@octokit/request-error': 3.0.3 - '@octokit/types': 9.3.2 - is-plain-object: 5.0.0 - node-fetch: 2.6.7(encoding@0.1.13) + '@octokit/endpoint': 9.0.6 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - transitivePeerDependencies: - - encoding - '@octokit/rest@19.0.11(encoding@0.1.13)': + '@octokit/rest@20.1.2': dependencies: - '@octokit/core': 4.2.4(encoding@0.1.13) - '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.4(encoding@0.1.13)) - '@octokit/plugin-request-log': 1.0.4(@octokit/core@4.2.4(encoding@0.1.13)) - '@octokit/plugin-rest-endpoint-methods': 7.2.3(@octokit/core@4.2.4(encoding@0.1.13)) - transitivePeerDependencies: - - encoding + '@octokit/core': 5.2.2 + '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) - '@octokit/tsconfig@1.0.2': {} - - '@octokit/types@10.0.0': + '@octokit/types@13.10.0': dependencies: - '@octokit/openapi-types': 18.1.1 + '@octokit/openapi-types': 24.2.0 - '@octokit/types@9.3.2': - dependencies: - '@octokit/openapi-types': 18.1.1 + '@pkgr/core@0.3.6': {} - '@parcel/watcher@2.0.4': + '@redocly/ajv@8.11.2': dependencies: - node-addon-api: 3.2.1 - node-gyp-build: 4.8.4 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@pkgr/core@0.1.1': {} + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 - '@pnpm/config.env-replace@1.1.0': {} + '@redocly/config@0.22.0': {} - '@pnpm/network.ca-file@1.0.2': + '@redocly/openapi-core@1.34.20': dependencies: - graceful-fs: 4.2.10 + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6 + js-levenshtein: 1.1.6 + js-yaml: 4.3.2 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color - '@pnpm/npm-conf@2.3.1': - dependencies: - '@pnpm/config.env-replace': 1.1.0 - '@pnpm/network.ca-file': 1.0.2 - config-chain: 1.1.13 + '@rollup/rollup-android-arm-eabi@4.62.3': + optional: true - '@readme/better-ajv-errors@1.6.0(ajv@8.17.1)': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/runtime': 7.26.0 - '@humanwhocodes/momoa': 2.0.4 - ajv: 8.17.1 - chalk: 4.1.2 - json-to-ast: 2.1.0 - jsonpointer: 5.0.1 - leven: 3.1.0 + '@rollup/rollup-android-arm64@4.62.3': + optional: true - '@readme/json-schema-ref-parser@1.2.0': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - call-me-maybe: 1.0.2 - js-yaml: 4.1.0 + '@rollup/rollup-darwin-arm64@4.62.3': + optional: true - '@readme/openapi-parser@2.6.0(openapi-types@12.1.3)': - dependencies: - '@apidevtools/swagger-methods': 3.0.2 - '@jsdevtools/ono': 7.1.3 - '@readme/better-ajv-errors': 1.6.0(ajv@8.17.1) - '@readme/json-schema-ref-parser': 1.2.0 - '@readme/openapi-schemas': 3.1.0 - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - call-me-maybe: 1.0.2 - openapi-types: 12.1.3 + '@rollup/rollup-darwin-x64@4.62.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.3': + optional: true - '@readme/openapi-schemas@3.1.0': {} + '@rollup/rollup-freebsd-x64@4.62.3': + optional: true - '@rollup/rollup-android-arm-eabi@4.31.0': + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': optional: true - '@rollup/rollup-android-arm64@4.31.0': + '@rollup/rollup-linux-arm-musleabihf@4.62.3': optional: true - '@rollup/rollup-darwin-arm64@4.31.0': + '@rollup/rollup-linux-arm64-gnu@4.62.3': optional: true - '@rollup/rollup-darwin-x64@4.31.0': + '@rollup/rollup-linux-arm64-musl@4.62.3': optional: true - '@rollup/rollup-freebsd-arm64@4.31.0': + '@rollup/rollup-linux-loong64-gnu@4.62.3': optional: true - '@rollup/rollup-freebsd-x64@4.31.0': + '@rollup/rollup-linux-loong64-musl@4.62.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.31.0': + '@rollup/rollup-linux-ppc64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.31.0': + '@rollup/rollup-linux-ppc64-musl@4.62.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.31.0': + '@rollup/rollup-linux-riscv64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.31.0': + '@rollup/rollup-linux-riscv64-musl@4.62.3': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.31.0': + '@rollup/rollup-linux-s390x-gnu@4.62.3': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': + '@rollup/rollup-linux-x64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.31.0': + '@rollup/rollup-linux-x64-musl@4.62.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.31.0': + '@rollup/rollup-openbsd-x64@4.62.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.31.0': + '@rollup/rollup-openharmony-arm64@4.62.3': optional: true - '@rollup/rollup-linux-x64-musl@4.31.0': + '@rollup/rollup-win32-arm64-msvc@4.62.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.31.0': + '@rollup/rollup-win32-ia32-msvc@4.62.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.31.0': + '@rollup/rollup-win32-x64-gnu@4.62.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.31.0': + '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true '@rtsao/scc@1.1.0': {} - '@sigstore/bundle@1.1.0': + '@sigstore/bundle@4.0.0': dependencies: - '@sigstore/protobuf-specs': 0.2.1 + '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/protobuf-specs@0.2.1': {} + '@sigstore/core@3.2.1': {} - '@sigstore/sign@1.0.0': + '@sigstore/protobuf-specs@0.5.1': {} + + '@sigstore/sign@4.1.1': dependencies: - '@sigstore/bundle': 1.1.0 - '@sigstore/protobuf-specs': 0.2.1 - make-fetch-happen: 11.1.1 + '@gar/promise-retry': 1.0.3 + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.1 + make-fetch-happen: 15.0.6 + proc-log: 6.1.0 transitivePeerDependencies: - supports-color - '@sigstore/tuf@1.0.3': + '@sigstore/tuf@4.0.2': dependencies: - '@sigstore/protobuf-specs': 0.2.1 - tuf-js: 1.1.7 + '@sigstore/protobuf-specs': 0.5.1 + tuf-js: 4.1.0 transitivePeerDependencies: - supports-color - '@sinclair/typebox@0.27.8': {} - - '@sindresorhus/is@5.6.0': {} - - '@sindresorhus/merge-streams@2.3.0': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': + '@sigstore/verify@3.1.1': dependencies: - '@sinonjs/commons': 3.0.1 - - '@szmarczak/http-timer@5.0.1': - dependencies: - defer-to-connect: 2.0.1 - - '@tootallnate/once@2.0.0': {} - - '@ts-rest/core@3.51.0(@types/node@22.10.7)(zod@3.24.1)': - optionalDependencies: - '@types/node': 22.10.7 - zod: 3.24.1 - - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.1 - '@tsconfig/node16@1.0.4': {} + '@sinclair/typebox@0.34.52': {} - '@tufjs/canonical-json@1.0.0': {} + '@standard-schema/spec@1.1.0': {} - '@tufjs/models@1.0.4': - dependencies: - '@tufjs/canonical-json': 1.0.0 - minimatch: 9.0.5 - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.26.5 - '@babel/types': 7.26.5 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 - - '@types/babel__generator@7.6.8': - dependencies: - '@babel/types': 7.26.5 + '@tufjs/canonical-json@2.0.0': {} - '@types/babel__template@7.4.4': + '@tufjs/models@4.1.0': dependencies: - '@babel/parser': 7.26.5 - '@babel/types': 7.26.5 + '@tufjs/canonical-json': 2.0.0 + minimatch: 10.2.6 - '@types/babel__traverse@7.20.6': + '@tybys/wasm-util@0.10.3': dependencies: - '@babel/types': 7.26.5 - - '@types/estree@1.0.6': {} + tslib: 2.8.1 + optional: true - '@types/fs-extra@9.0.13': + '@tybys/wasm-util@0.9.0': dependencies: - '@types/node': 22.10.7 + tslib: 2.8.1 - '@types/graceful-fs@4.1.9': + '@types/chai@5.2.3': dependencies: - '@types/node': 22.10.7 - - '@types/http-cache-semantics@4.0.4': {} - - '@types/istanbul-lib-coverage@2.0.6': {} + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 + '@types/deep-eql@4.0.2': {} - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} - '@types/minimatch@3.0.5': {} - '@types/minimist@1.2.5': {} - '@types/node@22.10.7': + '@types/node@22.20.4': dependencies: - undici-types: 6.20.0 + undici-types: 6.21.0 '@types/normalize-package-data@2.4.4': {} - '@types/semver@6.2.7': {} - - '@types/semver@7.5.8': {} - - '@types/stack-utils@2.0.3': {} - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.33': - dependencies: - '@types/yargs-parser': 21.0.3 + '@types/semver@7.8.0': {} '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - semver: 7.6.3 + semver: 7.8.5 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 @@ -6771,7 +5163,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) eslint: 8.57.1 optionalDependencies: typescript: 5.7.3 @@ -6787,7 +5179,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: @@ -6795,33 +5187,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@5.62.0': {} - '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.7.3)': - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.6.3 - tsutils: 3.21.0(typescript@5.7.3) - optionalDependencies: - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.6.3 + minimatch: 9.0.7 + semver: 7.8.5 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 @@ -6830,227 +5206,213 @@ snapshots: '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 + '@types/semver': 7.8.0 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.3) eslint: 8.57.1 - semver: 7.6.3 + semver: 7.8.5 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@5.62.0': - dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@6.21.0': dependencies: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 - '@typespec/compiler@0.63.0': - dependencies: - '@babel/code-frame': 7.25.9 - ajv: 8.17.1 - change-case: 5.4.4 - globby: 14.0.2 - mustache: 4.2.0 - picocolors: 1.1.1 - prettier: 3.3.3 - prompts: 2.4.2 - semver: 7.6.3 - temporal-polyfill: 0.2.5 - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - yaml: 2.5.1 - yargs: 17.7.2 + '@ungap/structured-clone@1.4.0': {} - '@typespec/http@0.63.0(@typespec/compiler@0.63.0)': - dependencies: - '@typespec/compiler': 0.63.0 + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true - '@typespec/openapi3@0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0))(@typespec/openapi@0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0)))(@typespec/versioning@0.63.0(@typespec/compiler@0.63.0))': - dependencies: - '@readme/openapi-parser': 2.6.0(openapi-types@12.1.3) - '@typespec/compiler': 0.63.0 - '@typespec/http': 0.63.0(@typespec/compiler@0.63.0) - '@typespec/openapi': 0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0)) - '@typespec/versioning': 0.63.0(@typespec/compiler@0.63.0) - openapi-types: 12.1.3 - yaml: 2.5.1 + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true - '@typespec/openapi@0.63.0(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0))': - dependencies: - '@typespec/compiler': 0.63.0 - '@typespec/http': 0.63.0(@typespec/compiler@0.63.0) + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true - '@typespec/rest@0.63.1(@typespec/compiler@0.63.0)(@typespec/http@0.63.0(@typespec/compiler@0.63.0))': - dependencies: - '@typespec/compiler': 0.63.0 - '@typespec/http': 0.63.0(@typespec/compiler@0.63.0) + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true - '@typespec/versioning@0.63.0(@typespec/compiler@0.63.0)': - dependencies: - '@typespec/compiler': 0.63.0 + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true - '@ungap/structured-clone@1.2.1': {} + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true - '@vitest/coverage-c8@0.33.0(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0))': + '@unrs/resolver-binding-wasm32-wasi@1.12.2': dependencies: - '@ampproject/remapping': 2.3.0 - c8: 7.14.0 - magic-string: 0.30.17 - picocolors: 1.1.1 - std-env: 3.8.0 - vitest: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true - '@vitest/coverage-v8@3.0.3(vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0))': + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: - '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + '@vitest/utils': 4.1.11 + ast-v8-to-istanbul: 1.0.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.8.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) - transitivePeerDependencies: - - supports-color + istanbul-reports: 3.2.0 + magicast: 0.5.5 + obug: 2.2.1 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) - '@vitest/expect@3.0.3': + '@vitest/expect@4.1.11': dependencies: - '@vitest/spy': 3.0.3 - '@vitest/utils': 3.0.3 - chai: 5.1.2 - tinyrainbow: 2.0.0 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 - '@vitest/mocker@3.0.3(vite@6.0.11(@types/node@22.10.7)(yaml@2.7.0))': + '@vitest/mocker@4.1.11(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1))': dependencies: - '@vitest/spy': 3.0.3 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: - vite: 6.0.11(@types/node@22.10.7)(yaml@2.7.0) + vite: 7.3.6(@types/node@22.20.4)(yaml@2.9.1) - '@vitest/pretty-format@3.0.3': + '@vitest/pretty-format@4.1.11': dependencies: - tinyrainbow: 2.0.0 + tinyrainbow: 3.1.1 - '@vitest/runner@3.0.3': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 3.0.3 - pathe: 2.0.2 + '@vitest/utils': 4.1.11 + pathe: 2.0.3 - '@vitest/snapshot@3.0.3': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 3.0.3 - magic-string: 0.30.17 - pathe: 2.0.2 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 - '@vitest/spy@3.0.3': - dependencies: - tinyspy: 3.0.2 + '@vitest/spy@4.1.11': {} - '@vitest/utils@3.0.3': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 3.0.3 - loupe: 3.1.2 - tinyrainbow: 2.0.0 + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 '@yarnpkg/lockfile@1.1.0': {} - '@yarnpkg/parsers@3.0.0-rc.46': - dependencies: - js-yaml: 3.14.1 - tslib: 2.8.1 - - '@zkochan/js-yaml@0.0.6': + '@zkochan/js-yaml@0.0.7': dependencies: argparse: 2.0.1 - '@zodios/core@10.9.6(axios@1.7.9)(zod@3.24.1)': - dependencies: - axios: 1.7.9 - zod: 3.24.1 - JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 through: 2.3.8 - abbrev@1.1.1: {} + abbrev@3.0.1: {} - acorn-jsx@5.3.2(acorn@8.14.0): - dependencies: - acorn: 8.14.0 + abbrev@4.0.0: {} - acorn-walk@8.3.4: + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.14.0 + acorn: 8.18.0 - acorn@8.14.0: {} + acorn@8.18.0: {} add-stream@1.0.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@7.2.0): dependencies: - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 + agent-base@7.1.4: {} aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-draft-04@1.0.0(ajv@8.17.1): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.20.0 - ajv@6.12.6: + 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 - ajv@8.17.1: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-escapes@5.0.0: dependencies: type-fest: 1.4.0 ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 + ansi-regex@6.4.0: {} ansi-styles@4.3.0: dependencies: @@ -7058,207 +5420,134 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - app-module-path@2.2.0: {} + ansi-styles@6.2.3: {} aproba@2.0.0: {} - are-we-there-yet@3.0.1: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - - arg@4.1.3: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-differ@3.0.0: {} - array-ify@1.0.0: {} - array-includes@3.1.8: + array-includes@3.2.0: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 is-string: 1.1.1 + math-intrinsics: 1.1.0 array-union@2.1.0: {} - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.2 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 arrify@1.0.1: {} - arrify@2.0.1: {} - - assert-plus@1.0.0: {} - assertion-error@2.0.1: {} - ast-module-types@5.0.0: {} - - astral-regex@2.0.0: {} + ast-v8-to-istanbul@1.0.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 - async@3.2.6: {} + async-function@1.0.0: {} asynckit@0.4.0: {} available-typed-arrays@1.0.7: dependencies: - possible-typed-array-names: 1.0.0 + possible-typed-array-names: 1.1.0 - axios@1.7.9: + axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - follow-redirects: 1.15.6 - form-data: 4.0.1 - proxy-from-env: 1.1.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug - - babel-jest@29.7.0(@babel/core@7.26.0): - dependencies: - '@babel/core': 7.26.0 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.26.0) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - supports-color - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.26.5 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color + balanced-match@1.0.2: {} - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.25.9 - '@babel/types': 7.26.5 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.6 - - babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): - dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) - - babel-preset-jest@29.6.3(@babel/core@7.26.0): - dependencies: - '@babel/core': 7.26.0 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + balanced-match@4.0.3: {} - balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} before-after-hook@2.2.3: {} + bin-links@5.0.0: + dependencies: + cmd-shim: 7.0.0 + npm-normalize-package-bin: 4.0.0 + proc-log: 5.0.0 + read-cmd-shim: 5.0.0 + write-file-atomic: 6.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - brace-expansion@1.1.11: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.1.7: dependencies: balanced-match: 1.0.2 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.24.4: - dependencies: - caniuse-lite: 1.0.30001695 - electron-to-chromium: 1.5.83 - node-releases: 2.0.19 - update-browserslist-db: 1.1.2(browserslist@4.24.4) - - bs-logger@0.2.6: + brace-expansion@5.0.12: dependencies: - fast-json-stable-stringify: 2.1.0 + balanced-match: 4.0.4 - bser@2.1.1: + braces@3.0.3: dependencies: - node-int64: 0.4.0 + fill-range: 7.1.1 buffer-from@1.1.2: {} @@ -7267,99 +5556,37 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - builtins@1.0.3: {} - - builtins@5.1.0: - dependencies: - semver: 7.6.3 - byte-size@8.1.1: {} - c8@7.14.0: - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.1.7 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 9.3.0 - yargs: 16.2.0 - yargs-parser: 20.2.9 - - cac@6.7.14: {} - - cacache@16.1.3: - dependencies: - '@npmcli/fs': 2.1.2 - '@npmcli/move-file': 2.0.1 - chownr: 2.0.0 - fs-minipass: 2.1.0 - glob: 8.1.0 - infer-owner: 1.0.4 - lru-cache: 7.18.3 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - mkdirp: 1.0.4 - p-map: 4.0.0 - promise-inflight: 1.0.1 - rimraf: 3.0.2 - ssri: 9.0.1 - tar: 6.2.0 - unique-filename: 2.0.1 - transitivePeerDependencies: - - bluebird - - cacache@17.1.4: + cacache@20.0.4: dependencies: - '@npmcli/fs': 3.1.1 + '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 - glob: 10.4.5 - lru-cache: 7.18.3 - minipass: 7.1.2 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 + glob: 13.0.6 + lru-cache: 11.5.2 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.7 minipass-pipeline: 1.2.4 - p-map: 4.0.0 - ssri: 10.0.6 - tar: 6.2.0 - unique-filename: 3.0.0 - - cacheable-lookup@7.0.0: {} - - cacheable-request@10.2.14: - dependencies: - '@types/http-cache-semantics': 4.0.4 - get-stream: 6.0.1 - http-cache-semantics: 4.1.1 - keyv: 4.5.4 - mimic-response: 4.0.0 - normalize-url: 8.0.1 - responselike: 3.0.0 + p-map: 7.0.6 + ssri: 13.0.1 - call-bind-apply-helpers@1.0.1: + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.8: + call-bind@1.0.9: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: + call-bound@1.0.4: dependencies: - call-bind-apply-helpers: 1.0.1 - get-intrinsic: 1.2.7 - - call-me-maybe@1.0.2: {} + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 callsites@3.1.0: {} @@ -7371,23 +5598,7 @@ snapshots: camelcase@5.3.1: {} - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001695: {} - - chai@5.1.2: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.2 - pathval: 2.0.0 - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + chai@6.2.2: {} chalk@4.1.0: dependencies: @@ -7401,21 +5612,17 @@ snapshots: chalk@5.3.0: {} - chalk@5.4.1: {} - - change-case@5.4.4: {} + chalk@5.6.2: {} - char-regex@1.0.2: {} + chalk@6.0.0: {} - chardet@0.7.0: {} + chardet@2.2.0: {} - check-error@2.1.1: {} - - chownr@2.0.0: {} + chownr@3.0.0: {} ci-info@3.9.0: {} - cjs-module-lexer@1.4.1: {} + ci-info@4.3.1: {} clean-stack@2.2.0: {} @@ -7433,15 +5640,13 @@ snapshots: cli-spinners@2.6.1: {} - cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} cli-truncate@3.1.0: dependencies: slice-ansi: 5.0.0 string-width: 5.1.2 - cli-width@3.0.0: {} - cli-width@4.1.0: {} cliui@7.0.4: @@ -7456,36 +5661,22 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - clone@1.0.4: {} - cmd-shim@6.0.1: {} - - co@4.6.0: {} - - code-error-fragment@0.0.230: {} - - collect-v8-coverage@1.0.2: {} + cmd-shim@6.0.3: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 + cmd-shim@7.0.0: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} color-support@1.1.3: {} + colorette@1.4.0: {} + colorette@2.0.20: {} columnify@1.6.0: @@ -7497,20 +5688,11 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@10.0.1: {} - commander@11.0.0: {} commander@12.1.0: {} - commander@7.2.0: {} - - commander@8.3.0: {} - - commander@9.5.0: - optional: true - - commondir@1.0.1: {} + common-ancestor-path@1.0.1: {} compare-func@2.0.0: dependencies: @@ -7526,11 +5708,6 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - config-chain@1.1.13: - dependencies: - ini: 1.3.8 - proto-list: 1.2.4 - console-control-strings@1.1.0: {} conventional-changelog-angular@7.0.0: @@ -7557,10 +5734,10 @@ snapshots: dependencies: conventional-commits-filter: 3.0.0 dateformat: 3.0.3 - handlebars: 4.7.8 + handlebars: 4.7.9 json-stringify-safe: 5.0.1 meow: 8.1.2 - semver: 7.6.3 + semver: 7.7.2 split: 1.0.1 conventional-commits-filter@3.0.0: @@ -7587,70 +5764,42 @@ snapshots: convert-source-map@2.0.0: {} - convict@6.2.4: - dependencies: - lodash.clonedeep: 4.5.0 - yargs-parser: 20.2.9 - - core-util-is@1.0.2: {} - core-util-is@1.0.3: {} - cosmiconfig@8.3.6(typescript@5.7.3): + cosmiconfig@9.0.0(typescript@5.7.3): dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 - path-type: 4.0.0 optionalDependencies: typescript: 5.7.3 - create-jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - create-require@1.1.1: {} - - cross-spawn@5.1.0: - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + dargs@7.0.0: {} data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + 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.3 + 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.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 @@ -7664,9 +5813,11 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.4.0: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 decamelize-keys@1.1.1: dependencies: @@ -7675,28 +5826,14 @@ snapshots: decamelize@1.2.0: {} - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - dedent@0.7.0: {} - dedent@1.5.3: {} - deep-eql@5.0.2: {} - - deep-extend@0.6.0: {} - deep-is@0.1.4: {} - deepmerge@4.3.1: {} - defaults@1.0.4: dependencies: clone: 1.0.4 - defer-to-connect@2.0.1: {} - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -7713,72 +5850,8 @@ snapshots: delayed-stream@1.0.0: {} - delegates@1.0.0: {} - - dependency-tree@10.0.9: - dependencies: - commander: 10.0.1 - filing-cabinet: 4.2.0 - precinct: 11.0.5 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - deprecation@2.3.1: {} - detect-indent@5.0.0: {} - - detect-indent@6.1.0: {} - - detect-newline@3.1.0: {} - - detective-amd@5.0.2: - dependencies: - ast-module-types: 5.0.0 - escodegen: 2.1.0 - get-amd-module-type: 5.0.1 - node-source-walk: 6.0.2 - - detective-cjs@5.0.1: - dependencies: - ast-module-types: 5.0.0 - node-source-walk: 6.0.2 - - detective-es6@4.0.1: - dependencies: - node-source-walk: 6.0.2 - - detective-postcss@6.1.3: - dependencies: - is-url: 1.2.4 - postcss: 8.5.1 - postcss-values-parser: 6.0.2(postcss@8.5.1) - - detective-sass@5.0.3: - dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 6.0.2 - - detective-scss@4.0.3: - dependencies: - gonzales-pe: 4.3.0 - node-source-walk: 6.0.2 - - detective-stylus@4.0.0: {} - - detective-typescript@11.2.0: - dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.3) - ast-module-types: 5.0.0 - node-source-walk: 6.0.2 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - - diff-sequences@29.6.3: {} - - diff@4.0.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7795,29 +5868,21 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv-expand@10.0.0: {} + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.7 - dotenv@16.3.2: {} + dotenv@16.4.7: {} dunder-proto@1.0.1: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - duplexer@0.1.2: {} - eastasianwidth@0.2.0: {} - ejs@3.1.10: - dependencies: - jake: 10.9.2 - - electron-to-chromium@1.5.83: {} - - emittery@0.13.1: {} - - emoji-regex@10.4.0: {} + ejs@5.0.1: {} emoji-regex@8.0.0: {} @@ -7828,46 +5893,48 @@ snapshots: iconv-lite: 0.6.3 optional: true - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 - enhanced-resolve@5.18.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - enquirer@2.3.6: dependencies: ansi-colors: 4.1.3 env-paths@2.2.1: {} - envinfo@7.8.1: {} + envinfo@7.13.0: {} err-code@2.0.3: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.9: + 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.8 - call-bound: 1.0.3 + 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.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.2.7 + es-to-primitive: 1.3.4 + 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 @@ -7875,167 +5942,167 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + 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.0 + is-weakref: 1.1.1 math-intrinsics: 1.1.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.7 - own-keys: 1.0.1 + own-keys: 1.0.2 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + 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.7 + typed-array-byte-offset: 1.0.5 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 + which-typed-array: 1.1.24 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-module-lexer@1.6.0: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.1: dependencies: es-errors: 1.3.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.2.7 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 - es-shim-unscopables@1.0.2: + es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 - es-to-primitive@1.3.0: + es-to-primitive@1.3.4: dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.24.2: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - - eslint-config-prettier@9.1.0(eslint@8.57.1): + eslint-config-prettier@9.1.2(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 - enhanced-resolve: 5.18.0 + debug: 4.4.3(supports-color@7.2.0) eslint: 8.57.1 - fast-glob: 3.3.3 - get-tsconfig: 4.9.0 - is-bun-module: 1.3.0 - is-glob: 4.0.3 - stable-hash: 0.0.4 + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-module-utils@2.14.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + hasown: 2.0.4 + is-core-module: 2.16.2 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: 1.0.9 + string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.7.3) @@ -8044,14 +6111,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.2.3(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.2): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.9): dependencies: eslint: 8.57.1 - prettier: 3.4.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.9.2 + prettier: 3.9.9 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 9.1.0(eslint@8.57.1) + eslint-config-prettier: 9.1.2(eslint@8.57.1) eslint-scope@7.2.2: dependencies: @@ -8062,24 +6129,24 @@ snapshots: eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.57.1 '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.1 - ajv: 6.12.6 + '@ungap/structured-clone': 1.4.0 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.3(supports-color@7.2.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -8091,11 +6158,11 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 + js-yaml: 4.3.2 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 strip-ansi: 6.0.1 @@ -8105,13 +6172,11 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} - - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -8123,32 +6188,18 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.9 esutils@2.0.3: {} - eval-estree-expression@2.0.3: {} - - eventemitter3@4.0.7: {} - - eventemitter3@5.0.1: {} - - execa@5.0.0: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.0 - human-signals: 2.1.0 - is-stream: 2.0.0 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - execa@5.1.1: + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + execa@5.0.0: dependencies: cross-spawn: 7.0.6 - get-stream: 6.0.1 + get-stream: 6.0.0 human-signals: 2.1.0 is-stream: 2.0.1 merge-stream: 2.0.0 @@ -8169,27 +6220,9 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 3.0.0 - exit@0.1.2: {} - - expect-type@1.1.0: {} - - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - - exponential-backoff@3.1.1: {} - - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 + expect-type@1.4.0: {} - extsprintf@1.4.1: {} + exponential-backoff@3.1.3: {} fast-deep-equal@3.1.3: {} @@ -8207,15 +6240,25 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.0.6: {} + fast-string-truncated-width@3.0.3: {} - fastq@1.18.0: + fast-string-width@3.0.2: dependencies: - reusify: 1.0.4 + fast-string-truncated-width: 3.0.3 - fb-watchman@2.0.2: + fast-uri@3.1.8: {} + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fastq@1.20.1: dependencies: - bser: 2.1.1 + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 figures@3.2.0: dependencies: @@ -8225,25 +6268,6 @@ snapshots: dependencies: flat-cache: 3.2.0 - filelist@1.0.4: - dependencies: - minimatch: 5.1.6 - - filing-cabinet@4.2.0: - dependencies: - app-module-path: 2.2.0 - commander: 10.0.1 - enhanced-resolve: 5.18.0 - is-relative-path: 1.0.2 - module-definition: 5.0.1 - module-lookup-amd: 8.0.5 - resolve: 1.22.10 - resolve-dependency-path: 3.0.2 - sass-lookup: 5.0.1 - stylus-lookup: 5.0.1 - tsconfig-paths: 4.2.0 - typescript: 5.7.3 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -8264,65 +6288,46 @@ snapshots: flat-cache@3.2.0: dependencies: - flatted: 3.3.2 + flatted: 3.4.3 keyv: 4.5.4 rimraf: 3.0.2 flat@5.0.2: {} - flatted@3.3.2: {} + flatted@3.4.3: {} - follow-redirects@1.15.6: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) - for-each@0.3.3: + for-each@0.3.5: dependencies: is-callable: 1.2.7 - foreground-child@2.0.0: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 3.0.7 - - foreground-child@3.3.0: + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@2.1.4: {} - - form-data@4.0.1: + 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 fs-constants@1.0.0: {} - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - - fs-extra@11.3.0: + fs-extra@11.4.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.1 universalify: 2.0.1 - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs-minipass@3.0.3: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 fs.realpath@1.0.0: {} @@ -8331,42 +6336,29 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.8: + function.prototype.name@1.2.0: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - define-properties: 1.2.1 + 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 - hasown: 2.0.2 + 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: {} - gauge@4.0.4: - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - - gensync@1.0.0-beta.2: {} - - get-amd-module-type@5.0.1: - dependencies: - ast-module-types: 5.0.0 - node-source-walk: 6.0.2 + generator-function@2.0.1: {} get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} + get-east-asian-width@1.7.0: {} - get-intrinsic@1.2.7: + get-intrinsic@1.3.0: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 @@ -8374,21 +6366,15 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 - get-own-enumerable-property-symbols@3.0.2: {} - - get-package-type@0.1.0: {} - get-pkg-repo@4.2.1: dependencies: '@hutson/parse-repository-url': 3.0.2 hosted-git-info: 4.1.0 through2: 2.0.5 - yargs: 16.2.0 - - get-port@5.1.1: {} + yargs: 16.2.2 get-proto@1.0.1: dependencies: @@ -8401,11 +6387,11 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 - get-tsconfig@4.9.0: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -8423,14 +6409,14 @@ snapshots: git-semver-tags@5.0.1: dependencies: meow: 8.1.2 - semver: 7.6.3 + semver: 7.7.2 git-up@7.0.0: dependencies: - is-ssh: 1.4.0 + is-ssh: 1.4.1 parse-url: 8.1.0 - git-url-parse@13.1.0: + git-url-parse@14.0.0: dependencies: git-up: 7.0.0 @@ -8446,50 +6432,30 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: + glob@11.1.0: dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.6 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 + path-scurry: 2.0.2 - glob@7.1.4: + glob@13.0.6: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.0.5 - once: 1.4.0 - path-is-absolute: 1.0.1 + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 - - glob@9.3.5: - dependencies: - fs.realpath: 1.0.0 - minimatch: 8.0.4 - minipass: 4.2.8 - path-scurry: 1.11.1 - - globals@11.12.0: {} - globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -8508,44 +6474,13 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@14.0.2: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 5.3.2 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 - - gonzales-pe@4.3.0: - dependencies: - minimist: 1.2.8 - gopd@1.2.0: {} - got@12.6.1: - dependencies: - '@sindresorhus/is': 5.6.0 - '@szmarczak/http-timer': 5.0.1 - cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 - decompress-response: 6.0.0 - form-data-encoder: 2.1.4 - get-stream: 6.0.1 - http2-wrapper: 2.2.1 - lowercase-keys: 3.0.0 - p-cancelable: 3.0.0 - responselike: 3.0.0 - - graceful-fs@4.2.10: {} - graceful-fs@4.2.11: {} - grapheme-splitter@1.0.4: {} - graphemer@1.4.0: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -8558,8 +6493,6 @@ snapshots: has-bigints@1.1.0: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -8578,45 +6511,42 @@ snapshots: has-unicode@2.0.1: {} - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 hosted-git-info@2.8.9: {} - hosted-git-info@3.0.8: - dependencies: - lru-cache: 6.0.0 - hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 - hosted-git-info@6.1.3: + hosted-git-info@9.0.3: dependencies: - lru-cache: 7.18.3 + lru-cache: 11.5.2 html-escaper@2.0.2: {} - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.2.0: {} - http-proxy-agent@5.0.0: + http-proxy-agent@7.0.2: dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.4.0 + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color - http2-wrapper@2.2.1: + https-proxy-agent@5.0.1(supports-color@7.2.0): dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 + agent-base: 6.0.2(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color - https-proxy-agent@5.0.1: + https-proxy-agent@7.0.6: dependencies: - agent-base: 6.0.2 - debug: 4.4.0 + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -8624,34 +6554,28 @@ snapshots: human-signals@4.3.1: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - husky@8.0.3: {} - iconv-lite@0.4.24: + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + optional: true - iconv-lite@0.6.3: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 - optional: true ieee754@1.2.1: {} - ignore-walk@5.0.1: + ignore-walk@8.0.0: dependencies: - minimatch: 5.1.6 - - ignore-walk@6.0.5: - dependencies: - minimatch: 9.0.5 + minimatch: 10.2.6 ignore@5.3.2: {} - import-fresh@3.3.0: + ignore@7.0.5: {} + + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 @@ -8661,17 +6585,10 @@ snapshots: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - imurmurhash@0.1.4: {} indent-string@4.0.0: {} - infer-owner@1.0.4: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -8681,69 +6598,50 @@ snapshots: ini@1.3.8: {} - init-package-json@5.0.0: + ini@6.0.0: {} + + init-package-json@8.2.2: dependencies: - npm-package-arg: 10.1.0 - promzard: 1.0.2 - read: 2.1.0 - read-package-json: 6.0.4 - semver: 7.6.3 + '@npmcli/package-json': 7.0.2 + npm-package-arg: 13.0.1 + promzard: 2.0.0 + read: 4.1.0 + semver: 7.7.2 validate-npm-package-license: 3.0.4 - validate-npm-package-name: 5.0.0 + validate-npm-package-name: 6.0.2 - inquirer@12.3.2(@types/node@22.10.7): + inquirer@12.9.6(@types/node@22.20.4): dependencies: - '@inquirer/core': 10.1.4(@types/node@22.10.7) - '@inquirer/prompts': 7.2.3(@types/node@22.10.7) - '@inquirer/type': 3.0.2(@types/node@22.10.7) - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.4) + '@inquirer/prompts': 7.10.1(@types/node@22.20.4) + '@inquirer/type': 3.0.10(@types/node@22.20.4) mute-stream: 2.0.0 - run-async: 3.0.0 - rxjs: 7.8.1 - - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.20.4 internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - ip-address@9.0.5: - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 + hasown: 2.0.4 + side-channel: 1.1.1 - ip@2.0.1: {} + ip-address@10.7.2: {} is-array-buffer@3.0.5: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-arrayish@0.2.1: {} - is-async-function@2.1.0: + is-async-function@2.1.1: dependencies: - call-bound: 1.0.3 + 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 @@ -8752,14 +6650,14 @@ snapshots: dependencies: has-bigints: 1.1.0 - is-boolean-object@1.2.1: + is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-bun-module@1.3.0: + is-bun-module@2.0.0: dependencies: - semver: 7.6.3 + semver: 7.8.5 is-callable@1.2.7: {} @@ -8767,38 +6665,41 @@ snapshots: dependencies: ci-info: 3.9.0 - is-core-module@2.16.1: + is-core-module@2.16.2: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + 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.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-docker@2.2.1: {} + 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.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} - is-generator-fn@2.1.0: {} - - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: - call-bound: 1.0.3 + 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 @@ -8811,53 +6712,39 @@ snapshots: is-interactive@2.0.0: {} - is-lambda@1.0.1: {} - is-map@2.0.3: {} + is-negative-zero@2.0.3: {} + is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} - is-obj@1.0.1: {} - is-obj@2.0.0: {} is-path-inside@3.0.3: {} is-plain-obj@1.1.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - - is-plain-object@5.0.0: {} - is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-regexp@1.0.0: {} - - is-relative-path@1.0.2: {} + hasown: 2.0.4 is-set@2.0.3: {} is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 - is-ssh@1.4.0: + is-ssh@1.4.1: dependencies: - protocols: 2.0.1 - - is-stream@2.0.0: {} + protocols: 2.0.2 is-stream@2.0.1: {} @@ -8865,12 +6752,12 @@ snapshots: is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 @@ -8880,28 +6767,22 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.24 is-unicode-supported@0.1.0: {} - is-unicode-supported@1.3.0: {} - is-unicode-supported@2.1.0: {} - is-url-superb@4.0.0: {} - - is-url@1.2.4: {} - is-weakmap@2.0.2: {} - is-weakref@1.1.0: + is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-wsl@2.2.0: dependencies: @@ -8911,434 +6792,65 @@ snapshots: isarray@2.0.5: {} - isexe@2.0.0: {} - - isobject@3.0.1: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.26.0 - '@babel/parser': 7.26.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.26.0 - '@babel/parser': 7.26.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.6.3 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.0 - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.1.7: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jake@10.9.2: - dependencies: - async: 3.2.6 - chalk: 4.1.2 - filelist: 1.0.4 - minimatch: 3.1.2 - - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.5.3 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): - dependencies: - '@babel/core': 7.26.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.10.7 - ts-node: 10.9.2(@types/node@22.10.7)(typescript@5.7.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 22.10.7 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.26.2 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.10 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color + isexe@2.0.0: {} - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - chalk: 4.1.2 - cjs-module-lexer: 1.4.1 - collect-v8-coverage: 1.0.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color + isexe@4.0.0: {} - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.26.0 - '@babel/generator': 7.26.5 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) - '@babel/types': 7.26.5 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.6.3 - transitivePeerDependencies: - - supports-color + istanbul-lib-coverage@3.2.2: {} - jest-util@29.7.0: + istanbul-lib-report@3.0.1: dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 - jest-validate@29.7.0: + istanbul-reports@3.2.0: dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 - jest-watcher@29.7.0: + jackspeak@4.2.3: dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.10.7 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 + '@isaacs/cliui': 9.0.0 - jest-worker@29.7.0: + jest-diff@30.4.1: dependencies: - '@types/node': 22.10.7 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 + '@jest/diff-sequences': 30.4.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.4.1 - jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + js-levenshtein@1.1.6: {} - jju@1.4.0: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 - jsbn@1.1.0: {} - - jsesc@3.1.0: {} - json-buffer@3.0.1: {} json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} - json-parse-even-better-errors@3.0.2: {} - - json-schema-diff@0.18.1: - dependencies: - ajv: 8.17.1 - commander: 10.0.1 - convict: 6.2.4 - json-schema-ref-parser: 9.0.9 - json-schema-spec-types: 0.1.2 - lodash: 4.17.21 - verror: 1.10.1 - - json-schema-ref-parser@9.0.9: - dependencies: - '@apidevtools/json-schema-ref-parser': 9.0.9 + json-parse-even-better-errors@4.0.0: {} - json-schema-spec-types@0.1.2: {} + json-parse-even-better-errors@5.0.0: {} json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} - json-stringify-safe@5.0.1: {} + json-stringify-nice@1.1.4: {} - json-to-ast@2.1.0: - dependencies: - code-error-fragment: 0.0.230 - grapheme-splitter: 1.0.4 + json-stringify-safe@5.0.1: {} json5@1.0.2: dependencies: @@ -9348,11 +6860,7 @@ snapshots: jsonc-parser@3.2.0: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - - jsonfile@6.1.0: + jsonfile@6.2.1: dependencies: universalify: 2.0.1 optionalDependencies: @@ -9360,7 +6868,9 @@ snapshots: jsonparse@1.3.1: {} - jsonpointer@5.0.1: {} + just-diff-apply@5.5.0: {} + + just-diff@6.0.2: {} keyv@4.5.4: dependencies: @@ -9368,117 +6878,103 @@ snapshots: kind-of@6.0.3: {} - kleur@3.0.3: {} - - lerna@7.4.2(encoding@0.1.13): + lerna@9.0.7(@types/node@22.20.4): dependencies: - '@lerna/child-process': 7.4.2 - '@lerna/create': 7.4.2(encoding@0.1.13)(typescript@5.7.3) - '@npmcli/run-script': 6.0.2 - '@nx/devkit': 16.10.0(nx@16.10.0) + '@npmcli/arborist': 9.1.6 + '@npmcli/package-json': 7.0.2 + '@npmcli/run-script': 10.0.3 + '@nx/devkit': 22.7.7(nx@22.7.10) '@octokit/plugin-enterprise-rest': 6.0.1 - '@octokit/rest': 19.0.11(encoding@0.1.13) + '@octokit/rest': 20.1.2 + aproba: 2.0.0 byte-size: 8.1.1 chalk: 4.1.0 - clone-deep: 4.0.1 - cmd-shim: 6.0.1 + ci-info: 4.3.1 + cmd-shim: 6.0.3 + color-support: 1.1.3 columnify: 1.6.0 + console-control-strings: 1.1.0 conventional-changelog-angular: 7.0.0 conventional-changelog-core: 5.0.1 conventional-recommended-bump: 7.0.1 - cosmiconfig: 8.3.6(typescript@5.7.3) - dedent: 0.7.0 - envinfo: 7.8.1 + cosmiconfig: 9.0.0(typescript@5.7.3) + dedent: 1.5.3 + envinfo: 7.13.0 execa: 5.0.0 - fs-extra: 11.3.0 - get-port: 5.1.1 + fs-extra: 11.4.0 get-stream: 6.0.0 - git-url-parse: 13.1.0 - glob-parent: 5.1.2 - globby: 11.1.0 - graceful-fs: 4.2.11 + git-url-parse: 14.0.0 + glob-parent: 6.0.2 has-unicode: 2.0.1 import-local: 3.1.0 ini: 1.3.8 - init-package-json: 5.0.0 - inquirer: 8.2.6 + init-package-json: 8.2.2 + inquirer: 12.9.6(@types/node@22.20.4) is-ci: 3.0.1 - is-stream: 2.0.0 - jest-diff: 29.7.0 - js-yaml: 4.1.0 - libnpmaccess: 7.0.2 - libnpmpublish: 7.3.0 + jest-diff: 30.4.1 + js-yaml: 4.3.2 + libnpmaccess: 10.0.3 + libnpmpublish: 11.1.2 load-json-file: 6.2.0 - lodash: 4.17.21 - make-dir: 4.0.0 - minimatch: 3.0.5 - multimatch: 5.0.0 - node-fetch: 2.6.7(encoding@0.1.13) - npm-package-arg: 8.1.1 - npm-packlist: 5.1.1 - npm-registry-fetch: 14.0.5 - npmlog: 6.0.2 - nx: 16.10.0 + make-fetch-happen: 15.0.2 + minimatch: 3.1.4 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-registry-fetch: 19.1.0 + nx: 22.7.10 p-map: 4.0.0 p-map-series: 2.1.0 p-pipe: 3.1.0 p-queue: 6.6.2 p-reduce: 2.1.0 p-waterfall: 2.1.1 - pacote: 15.2.0 - pify: 5.0.0 + pacote: 21.5.1 read-cmd-shim: 4.0.0 - read-package-json: 6.0.4 - resolve-from: 5.0.0 - rimraf: 4.4.1 - semver: 7.6.3 + semver: 7.7.2 signal-exit: 3.0.7 slash: 3.0.0 - ssri: 9.0.1 - strong-log-transformer: 2.1.0 - tar: 6.1.11 - temp-dir: 1.0.0 + ssri: 12.0.0 + string-width: 4.2.3 + tar: 7.5.21 + through: 2.3.8 + tinyglobby: 0.2.12 typescript: 5.7.3 upath: 2.0.1 - uuid: 9.0.1 validate-npm-package-license: 3.0.4 - validate-npm-package-name: 5.0.0 + validate-npm-package-name: 6.0.2 + wide-align: 1.1.5 write-file-atomic: 5.0.1 - write-pkg: 4.0.0 - yargs: 16.2.0 - yargs-parser: 20.2.4 + yargs: 17.7.2 + yargs-parser: 21.1.1 transitivePeerDependencies: - '@swc-node/register' - '@swc/core' - - bluebird - - debug - - encoding + - '@types/node' + - babel-plugin-macros - supports-color - leven@3.1.0: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - libnpmaccess@7.0.2: + libnpmaccess@10.0.3: dependencies: - npm-package-arg: 10.1.0 - npm-registry-fetch: 14.0.5 + npm-package-arg: 13.0.1 + npm-registry-fetch: 19.1.0 transitivePeerDependencies: - supports-color - libnpmpublish@7.3.0: + libnpmpublish@11.1.2: dependencies: - ci-info: 3.9.0 - normalize-package-data: 5.0.0 - npm-package-arg: 10.1.0 - npm-registry-fetch: 14.0.5 - proc-log: 3.0.0 - semver: 7.6.3 - sigstore: 1.9.0 - ssri: 10.0.6 + '@npmcli/package-json': 7.0.2 + ci-info: 4.3.1 + npm-package-arg: 13.0.1 + npm-registry-fetch: 19.1.0 + proc-log: 5.0.0 + semver: 7.7.2 + sigstore: 4.1.1 + ssri: 12.0.0 transitivePeerDependencies: - supports-color @@ -9486,7 +6982,7 @@ snapshots: lines-and-columns@1.2.4: {} - lines-and-columns@2.0.4: {} + lines-and-columns@2.0.3: {} lint-staged@14.0.1(enquirer@2.3.6): dependencies: @@ -9496,10 +6992,10 @@ snapshots: execa: 7.2.0 lilconfig: 2.1.0 listr2: 6.6.1(enquirer@2.3.6) - micromatch: 4.0.5 + micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.3.1 + yaml: 2.9.1 transitivePeerDependencies: - enquirer - supports-color @@ -9508,7 +7004,7 @@ snapshots: dependencies: cli-truncate: 3.1.0 colorette: 2.0.20 - eventemitter3: 5.0.1 + eventemitter3: 5.0.4 log-update: 5.0.1 rfdc: 1.4.1 wrap-ansi: 8.1.0 @@ -9542,148 +7038,83 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.clonedeep@4.5.0: {} - - lodash.get@4.4.2: {} - - lodash.isequal@4.5.0: {} - lodash.ismatch@4.4.0: {} - lodash.memoize@4.1.2: {} - lodash.merge@4.6.2: {} - lodash.truncate@4.4.2: {} - - lodash@4.17.21: {} - log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-symbols@6.0.0: + log-symbols@7.0.1: dependencies: - chalk: 5.4.1 - is-unicode-supported: 1.3.0 + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 log-update@5.0.1: dependencies: ansi-escapes: 5.0.0 cli-cursor: 4.0.0 slice-ansi: 5.0.0 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 wrap-ansi: 8.1.0 - loupe@3.1.2: {} - - lowercase-keys@3.0.0: {} - - lru-cache@10.4.3: {} - - lru-cache@4.1.5: - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 + lru-cache@11.5.2: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 + lru-cache@11.5.3: {} lru-cache@6.0.0: dependencies: yallist: 4.0.0 - lru-cache@7.18.3: {} - - madge@7.0.0(typescript@5.7.3): - dependencies: - chalk: 4.1.2 - commander: 7.2.0 - commondir: 1.0.1 - debug: 4.4.0 - dependency-tree: 10.0.9 - ora: 5.4.1 - pluralize: 8.0.0 - precinct: 11.0.5 - pretty-ms: 7.0.1 - rc: 1.2.8 - stream-to-array: 2.3.0 - ts-graphviz: 1.8.2 - walkdir: 0.4.1 - optionalDependencies: - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - magicast@0.3.5: + magic-string@0.30.21: dependencies: - '@babel/parser': 7.26.5 - '@babel/types': 7.26.5 - source-map-js: 1.2.1 + '@jridgewell/sourcemap-codec': 1.6.0 - make-dir@2.1.0: + magicast@0.5.5: dependencies: - pify: 4.0.1 - semver: 5.7.2 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.2 make-dir@4.0.0: dependencies: - semver: 7.6.3 - - make-error@1.3.6: {} + semver: 7.8.5 - make-fetch-happen@10.2.1: + make-fetch-happen@15.0.2: dependencies: - agentkeepalive: 4.6.0 - cacache: 16.1.3 - http-cache-semantics: 4.1.1 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 7.18.3 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-fetch: 2.1.2 - minipass-flush: 1.0.5 + '@npmcli/agent': 4.0.2 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 4.0.1 + minipass-flush: 1.0.7 minipass-pipeline: 1.2.4 - negotiator: 0.6.4 + negotiator: 1.0.0 + proc-log: 5.0.0 promise-retry: 2.0.1 - socks-proxy-agent: 7.0.0 - ssri: 9.0.1 + ssri: 12.0.0 transitivePeerDependencies: - - bluebird - supports-color - make-fetch-happen@11.1.1: - dependencies: - agentkeepalive: 4.6.0 - cacache: 17.1.4 - http-cache-semantics: 4.1.1 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 7.18.3 - minipass: 5.0.0 - minipass-fetch: 3.0.5 - minipass-flush: 1.0.5 + make-fetch-happen@15.0.6: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.2 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.7 minipass-pipeline: 1.2.4 - negotiator: 0.6.4 - promise-retry: 2.0.1 - socks-proxy-agent: 7.0.0 - ssri: 10.0.6 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 transitivePeerDependencies: - supports-color - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -9708,15 +7139,10 @@ snapshots: merge2@1.4.1: {} - micromatch@4.0.5: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -9730,35 +7156,31 @@ snapshots: mimic-function@5.0.1: {} - mimic-response@3.1.0: {} - - mimic-response@4.0.0: {} - min-indent@1.0.1: {} - minimatch@3.0.5: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 5.0.12 - minimatch@3.1.2: + minimatch@10.2.6: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 5.0.12 - minimatch@5.1.6: + minimatch@3.1.4: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.21 - minimatch@8.0.4: + minimatch@3.1.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.21 - minimatch@9.0.3: + minimatch@5.1.9: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.1.7 - minimatch@9.0.5: + minimatch@9.0.7: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 5.0.12 minimist-options@4.1.0: dependencies: @@ -9768,35 +7190,30 @@ snapshots: minimist@1.2.8: {} - minipass-collect@1.0.2: + minipass-collect@2.0.1: dependencies: - minipass: 3.3.6 + minipass: 7.1.3 - minipass-fetch@2.1.2: + minipass-fetch@4.0.1: dependencies: - minipass: 3.3.6 + minipass: 7.1.3 minipass-sized: 1.0.3 - minizlib: 2.1.2 + minizlib: 3.1.0 optionalDependencies: encoding: 0.1.13 - minipass-fetch@3.0.5: + minipass-fetch@5.0.2: dependencies: - minipass: 7.1.2 - minipass-sized: 1.0.3 - minizlib: 2.1.2 + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 optionalDependencies: - encoding: 0.1.13 + iconv-lite: 0.7.3 - minipass-flush@1.0.5: + minipass-flush@1.0.7: dependencies: minipass: 3.3.6 - minipass-json-stream@1.0.2: - dependencies: - jsonparse: 1.3.1 - minipass: 3.3.6 - minipass-pipeline@1.2.4: dependencies: minipass: 3.3.6 @@ -9805,187 +7222,131 @@ snapshots: dependencies: minipass: 3.3.6 - minipass@3.3.6: + minipass-sized@2.0.0: dependencies: - yallist: 4.0.0 - - minipass@4.2.8: {} - - minipass@5.0.0: {} + minipass: 7.1.3 - minipass@7.1.2: {} - - minizlib@2.1.2: + minipass@3.3.6: dependencies: - minipass: 3.3.6 yallist: 4.0.0 - mkdirp@1.0.4: {} - - modify-values@1.0.1: {} + minipass@7.1.3: {} - module-definition@5.0.1: + minizlib@3.1.0: dependencies: - ast-module-types: 5.0.0 - node-source-walk: 6.0.2 + minipass: 7.1.3 - module-lookup-amd@8.0.5: - dependencies: - commander: 10.0.1 - glob: 7.2.3 - requirejs: 2.3.7 - requirejs-config-file: 4.0.0 + modify-values@1.0.1: {} ms@2.1.2: {} ms@2.1.3: {} - multimatch@5.0.0: - dependencies: - '@types/minimatch': 3.0.5 - array-differ: 3.0.0 - array-union: 2.1.0 - arrify: 2.0.1 - minimatch: 3.0.5 - - mustache@4.2.0: {} - - mute-stream@0.0.8: {} + mute-stream@2.0.0: {} - mute-stream@1.0.0: {} + mute-stream@3.0.0: {} - mute-stream@2.0.0: {} + nanoid@3.3.19: {} - nanoid@3.3.8: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - negotiator@0.6.4: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} - node-addon-api@3.2.1: {} - - node-fetch@2.6.7(encoding@0.1.13): + node-exports-info@1.6.2: dependencies: - whatwg-url: 5.0.0 - optionalDependencies: - encoding: 0.1.13 - - node-gyp-build@4.8.4: {} + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 - node-gyp@9.4.1: + node-gyp@12.4.0: dependencies: env-paths: 2.2.1 - exponential-backoff: 3.1.1 - glob: 7.2.3 + exponential-backoff: 3.1.3 graceful-fs: 4.2.11 - make-fetch-happen: 10.2.1 - nopt: 6.0.0 - npmlog: 6.0.2 - rimraf: 3.0.2 - semver: 7.6.3 - tar: 6.2.0 - which: 2.0.2 - transitivePeerDependencies: - - bluebird - - supports-color - - node-int64@0.4.0: {} - - node-machine-id@1.1.12: {} + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.2 + tar: 7.5.21 + tinyglobby: 0.2.17 + undici: 6.29.0 + which: 6.0.1 - node-releases@2.0.19: {} - - node-source-walk@6.0.2: + nopt@8.1.0: dependencies: - '@babel/parser': 7.26.5 + abbrev: 3.0.1 - nopt@6.0.0: + nopt@9.0.0: dependencies: - abbrev: 1.1.1 + abbrev: 4.0.0 normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.12 semver: 5.7.2 validate-npm-package-license: 3.0.4 normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.16.1 - semver: 7.6.3 + is-core-module: 2.16.2 + semver: 7.7.2 validate-npm-package-license: 3.0.4 - normalize-package-data@5.0.0: + npm-bundled@4.0.0: dependencies: - hosted-git-info: 6.1.3 - is-core-module: 2.16.1 - semver: 7.6.3 - validate-npm-package-license: 3.0.4 - - normalize-path@3.0.0: {} - - normalize-url@8.0.1: {} + npm-normalize-package-bin: 4.0.0 - npm-bundled@1.1.2: + npm-bundled@5.0.0: dependencies: - npm-normalize-package-bin: 1.0.1 + npm-normalize-package-bin: 5.0.0 - npm-bundled@3.0.1: + npm-install-checks@7.1.2: dependencies: - npm-normalize-package-bin: 3.0.1 + semver: 7.7.2 - npm-install-checks@6.3.0: + npm-install-checks@8.0.0: dependencies: - semver: 7.6.3 + semver: 7.8.5 - npm-normalize-package-bin@1.0.1: {} + npm-normalize-package-bin@4.0.0: {} - npm-normalize-package-bin@3.0.1: {} - - npm-package-arg@10.1.0: - dependencies: - hosted-git-info: 6.1.3 - proc-log: 3.0.0 - semver: 7.6.3 - validate-npm-package-name: 5.0.0 - - npm-package-arg@8.1.1: - dependencies: - hosted-git-info: 3.0.8 - semver: 7.6.3 - validate-npm-package-name: 3.0.0 + npm-normalize-package-bin@5.0.0: {} - npm-packlist@5.1.1: + npm-package-arg@13.0.1: dependencies: - glob: 8.1.0 - ignore-walk: 5.0.1 - npm-bundled: 1.1.2 - npm-normalize-package-bin: 1.0.1 + hosted-git-info: 9.0.3 + proc-log: 5.0.0 + semver: 7.7.2 + validate-npm-package-name: 6.0.2 - npm-packlist@7.0.4: + npm-packlist@10.0.3: dependencies: - ignore-walk: 6.0.5 + ignore-walk: 8.0.0 + proc-log: 6.1.0 - npm-pick-manifest@8.0.2: + npm-pick-manifest@11.0.3: dependencies: - npm-install-checks: 6.3.0 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 10.1.0 - semver: 7.6.3 + npm-install-checks: 8.0.0 + npm-normalize-package-bin: 5.0.0 + npm-package-arg: 13.0.1 + semver: 7.8.5 - npm-registry-fetch@14.0.5: + npm-registry-fetch@19.1.0: dependencies: - make-fetch-happen: 11.1.1 - minipass: 5.0.0 - minipass-fetch: 3.0.5 - minipass-json-stream: 1.0.2 - minizlib: 2.1.2 - npm-package-arg: 10.1.0 - proc-log: 3.0.0 + '@npmcli/redact': 3.2.2 + jsonparse: 1.3.1 + make-fetch-happen: 15.0.2 + minipass: 7.1.3 + minipass-fetch: 4.0.1 + minizlib: 3.1.0 + npm-package-arg: 13.0.1 + proc-log: 5.0.0 transitivePeerDependencies: - supports-color @@ -9997,97 +7358,175 @@ snapshots: dependencies: path-key: 4.0.0 - npmlog@6.0.2: - dependencies: - are-we-there-yet: 3.0.1 - console-control-strings: 1.1.0 - gauge: 4.0.4 - set-blocking: 2.0.0 - - nx@16.10.0: + nx@22.7.10: dependencies: - '@nrwl/tao': 16.10.0 - '@parcel/watcher': 2.0.4 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@emnapi/wasi-threads': 1.0.4 + '@jest/diff-sequences': 30.0.1 + '@napi-rs/wasm-runtime': 0.2.4 + '@tybys/wasm-util': 0.9.0 '@yarnpkg/lockfile': 1.1.0 - '@yarnpkg/parsers': 3.0.0-rc.46 - '@zkochan/js-yaml': 0.0.6 - axios: 1.7.9 + '@zkochan/js-yaml': 0.0.7 + agent-base: 6.0.2(supports-color@7.2.0) + ansi-colors: 4.1.3 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + argparse: 2.0.1 + asynckit: 0.4.0 + axios: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + balanced-match: 4.0.3 + base64-js: 1.5.1 + bl: 4.1.0 + brace-expansion: 5.0.12 + buffer: 5.7.1 + call-bind-apply-helpers: 1.0.2 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 8.0.1 - dotenv: 16.3.2 - dotenv-expand: 10.0.0 + clone: 1.0.4 + color-convert: 2.0.1 + color-name: 1.1.4 + combined-stream: 1.0.8 + debug: 4.4.3(supports-color@7.2.0) + defaults: 1.0.4 + define-lazy-prop: 2.0.0 + delayed-stream: 1.0.0 + dotenv: 16.4.7 + dotenv-expand: 12.0.3 + dunder-proto: 1.0.1 + ejs: 5.0.1 + emoji-regex: 8.0.0 + end-of-stream: 1.4.5 enquirer: 2.3.6 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + escalade: 3.2.0 + escape-string-regexp: 1.0.5 figures: 3.2.0 flat: 5.0.2 - fs-extra: 11.3.0 - glob: 7.1.4 - ignore: 5.3.2 - jest-diff: 29.7.0 - js-yaml: 4.1.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + fs-constants: 1.0.0 + function-bind: 1.1.2 + get-caller-file: 2.0.5 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + has-flag: 4.0.0 + has-symbols: 1.1.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + ieee754: 1.2.1 + ignore: 7.0.5 + inherits: 2.0.4 + is-docker: 2.2.1 + is-fullwidth-code-point: 3.0.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + is-wsl: 2.2.0 + json5: 2.2.3 jsonc-parser: 3.2.0 - lines-and-columns: 2.0.4 - minimatch: 3.0.5 - node-machine-id: 1.1.12 + lines-and-columns: 2.0.3 + log-symbols: 4.1.0 + math-intrinsics: 1.1.0 + mime-db: 1.52.0 + mime-types: 2.1.35 + mimic-fn: 2.1.0 + minimatch: 10.2.5 + minimist: 1.2.8 + ms: 2.1.3 npm-run-path: 4.0.1 + once: 1.4.0 + onetime: 5.1.2 open: 8.4.2 - semver: 7.5.3 + ora: 5.3.0 + path-key: 3.1.1 + picocolors: 1.1.1 + proxy-from-env: 2.1.0 + readable-stream: 3.6.2 + require-directory: 2.1.1 + resolve.exports: 2.0.3 + restore-cursor: 3.1.0 + safe-buffer: 5.2.1 + semver: 7.7.4 + signal-exit: 3.0.7 + smol-toml: 1.9.0 string-width: 4.2.3 - strong-log-transformer: 2.1.0 + string_decoder: 1.3.0 + strip-ansi: 6.0.1 + strip-bom: 3.0.0 + supports-color: 7.2.0 tar-stream: 2.2.0 - tmp: 0.2.3 + tmp: 0.2.7 + tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tslib: 2.8.1 - v8-compile-cache: 2.3.0 + util-deprecate: 1.0.2 + wcwidth: 1.0.1 + wrap-ansi: 7.0.0 + wrappy: 1.0.2 + y18n: 5.0.8 + yaml: 2.9.0 yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 16.10.0 - '@nx/nx-darwin-x64': 16.10.0 - '@nx/nx-freebsd-x64': 16.10.0 - '@nx/nx-linux-arm-gnueabihf': 16.10.0 - '@nx/nx-linux-arm64-gnu': 16.10.0 - '@nx/nx-linux-arm64-musl': 16.10.0 - '@nx/nx-linux-x64-gnu': 16.10.0 - '@nx/nx-linux-x64-musl': 16.10.0 - '@nx/nx-win32-arm64-msvc': 16.10.0 - '@nx/nx-win32-x64-msvc': 16.10.0 - transitivePeerDependencies: - - debug - - object-inspect@1.13.3: {} + '@nx/nx-darwin-arm64': 22.7.10 + '@nx/nx-darwin-x64': 22.7.10 + '@nx/nx-freebsd-x64': 22.7.10 + '@nx/nx-linux-arm-gnueabihf': 22.7.10 + '@nx/nx-linux-arm64-gnu': 22.7.10 + '@nx/nx-linux-arm64-musl': 22.7.10 + '@nx/nx-linux-x64-gnu': 22.7.10 + '@nx/nx-linux-x64-musl': 22.7.10 + '@nx/nx-win32-arm64-msvc': 22.7.10 + '@nx/nx-win32-x64-msvc': 22.7.10 + + object-inspect@1.13.4: {} object-keys@1.1.1: {} object.assign@4.1.7: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 + call-bind: 1.0.9 + call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.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.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 object.groupby@1.0.3: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.2 object.values@1.2.1: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 + call-bind: 1.0.9 + call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 + + obug@2.2.1: {} once@1.4.0: dependencies: @@ -10105,62 +7544,11 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - openapi-diff@0.23.7(openapi-types@12.1.3): - dependencies: - axios: 1.7.9 - commander: 8.3.0 - js-yaml: 4.1.0 - json-schema-diff: 0.18.1 - jsonpointer: 5.0.1 - lodash: 4.17.21 - openapi3-ts: 2.0.2 - swagger-parser: 10.0.3(openapi-types@12.1.3) - verror: 1.10.1 - transitivePeerDependencies: - - debug - - openapi-types - - openapi-types@12.1.3: {} - - openapi-zod-client@1.18.2: - dependencies: - '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@liuli-util/fs-extra': 0.1.0 - '@zodios/core': 10.9.6(axios@1.7.9)(zod@3.24.1) - axios: 1.7.9 - cac: 6.7.14 - handlebars: 4.7.8 - openapi-types: 12.1.3 - openapi3-ts: 3.1.0 - pastable: 2.2.1 - prettier: 2.8.8 - tanu: 0.1.13 - ts-pattern: 5.6.2 - whence: 2.0.1 - zod: 3.24.1 - transitivePeerDependencies: - - debug - - react - - supports-color - - xstate - - openapi3-ts@2.0.2: - dependencies: - yaml: 1.10.2 - - openapi3-ts@3.1.0: - dependencies: - yaml: 2.7.0 - - openapi3-ts@4.4.0: + open@8.4.2: dependencies: - yaml: 2.7.0 + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 optionator@0.9.4: dependencies: @@ -10171,40 +7559,35 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@5.4.1: + ora@5.3.0: dependencies: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.9.2 + cli-spinners: 2.6.1 is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - ora@8.1.1: + ora@9.4.1: dependencies: - chalk: 5.4.1 + chalk: 5.6.2 cli-cursor: 5.0.0 - cli-spinners: 2.9.2 + cli-spinners: 3.4.0 is-interactive: 2.0.0 is-unicode-supported: 2.1.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.3.0 - os-tmpdir@1.0.2: {} - - own-keys@1.0.1: + own-keys@1.0.2: dependencies: - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 - p-cancelable@3.0.0: {} - p-finally@1.0.0: {} p-limit@1.3.0: @@ -10237,6 +7620,8 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-map@7.0.6: {} + p-pipe@3.1.0: {} p-queue@6.6.2: @@ -10260,72 +7645,57 @@ snapshots: package-json-from-dist@1.0.1: {} - package-json@8.1.1: - dependencies: - got: 12.6.1 - registry-auth-token: 5.0.3 - registry-url: 6.0.1 - semver: 7.6.3 - - pacote@15.2.0: + pacote@21.5.1: dependencies: - '@npmcli/git': 4.1.0 - '@npmcli/installed-package-contents': 2.1.0 - '@npmcli/promise-spawn': 6.0.2 - '@npmcli/run-script': 6.0.2 - cacache: 17.1.4 + '@gar/promise-retry': 1.0.3 + '@npmcli/git': 7.0.2 + '@npmcli/installed-package-contents': 4.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/promise-spawn': 9.0.1 + '@npmcli/run-script': 10.0.3 + cacache: 20.0.4 fs-minipass: 3.0.3 - minipass: 5.0.0 - npm-package-arg: 10.1.0 - npm-packlist: 7.0.4 - npm-pick-manifest: 8.0.2 - npm-registry-fetch: 14.0.5 - proc-log: 3.0.0 - promise-retry: 2.0.1 - read-package-json: 6.0.4 - read-package-json-fast: 3.0.2 - sigstore: 1.9.0 - ssri: 10.0.6 - tar: 6.2.0 + minipass: 7.1.3 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.0 + proc-log: 6.1.0 + sigstore: 4.1.1 + ssri: 13.0.1 + tar: 7.5.21 transitivePeerDependencies: - - bluebird - supports-color parent-module@1.0.1: dependencies: callsites: 3.1.0 - parse-github-url@1.0.3: {} + parse-conflict-json@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + just-diff: 6.0.2 + just-diff-apply: 5.5.0 parse-json@4.0.0: dependencies: - error-ex: 1.3.2 + error-ex: 1.3.4 json-parse-better-errors: 1.0.2 parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 - error-ex: 1.3.2 + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-ms@2.1.0: {} - - parse-path@7.0.0: + parse-path@7.1.0: dependencies: - protocols: 2.0.1 + protocols: 2.0.2 parse-url@8.1.0: dependencies: - parse-path: 7.0.0 - - pastable@2.2.1: - dependencies: - '@babel/core': 7.26.0 - ts-toolbelt: 9.6.0 - type-fest: 3.13.1 - transitivePeerDependencies: - - supports-color + parse-path: 7.1.0 path-exists@3.0.0: {} @@ -10339,10 +7709,10 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: + path-scurry@2.0.2: dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 + lru-cache: 11.5.3 + minipass: 7.1.3 path-type@3.0.0: dependencies: @@ -10350,15 +7720,13 @@ snapshots: path-type@4.0.0: {} - path-type@5.0.0: {} - - pathe@2.0.2: {} - - pathval@2.0.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} + + picomatch@4.0.7: {} pidtree@0.6.0: {} @@ -10366,134 +7734,78 @@ snapshots: pify@3.0.0: {} - pify@4.0.1: {} - - pify@5.0.0: {} - - pirates@4.0.6: {} - pkg-dir@4.2.0: dependencies: find-up: 4.1.0 pluralize@8.0.0: {} - possible-typed-array-names@1.0.0: {} + possible-typed-array-names@1.1.0: {} - postcss-values-parser@6.0.2(postcss@8.5.1): + postcss-selector-parser@7.1.6: dependencies: - color-name: 1.1.4 - is-url-superb: 4.0.0 - postcss: 8.5.1 - quote-unquote: 1.0.0 + cssesc: 3.0.0 + util-deprecate: 1.0.2 - postcss@8.5.1: + postcss@8.5.24: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.19 picocolors: 1.1.1 - source-map-js: 1.2.1 - - precinct@11.0.5: - dependencies: - '@dependents/detective-less': 4.1.0 - commander: 10.0.1 - detective-amd: 5.0.2 - detective-cjs: 5.0.1 - detective-es6: 4.0.1 - detective-postcss: 6.1.3 - detective-sass: 5.0.3 - detective-scss: 4.0.3 - detective-stylus: 4.0.0 - detective-typescript: 11.2.0 - module-definition: 5.0.1 - node-source-walk: 6.0.2 - transitivePeerDependencies: - - supports-color + source-map-js: 1.2.2 prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@2.8.8: {} + prettier@3.9.9: {} - prettier@3.3.3: {} - - prettier@3.4.2: {} - - pretty-format@29.7.0: + pretty-format@30.4.1: dependencies: - '@jest/schemas': 29.6.3 + '@jest/schemas': 30.4.1 ansi-styles: 5.2.0 - react-is: 18.3.1 + react-is-18: react-is@18.3.1 + react-is-19: react-is@19.2.8 - pretty-ms@7.0.1: - dependencies: - parse-ms: 2.1.0 + proc-log@5.0.0: {} - proc-log@3.0.0: {} + proc-log@6.1.0: {} process-nextick-args@2.0.1: {} - promise-inflight@1.0.1: {} + proggy@3.0.0: {} + + promise-all-reject-late@1.0.1: {} + + promise-call-limit@3.0.2: {} promise-retry@2.0.1: dependencies: err-code: 2.0.3 retry: 0.12.0 - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - promzard@1.0.2: + promzard@2.0.0: dependencies: - read: 3.0.1 - - proto-list@1.2.4: {} + read: 4.1.0 - protocols@2.0.1: {} + protocols@2.0.2: {} - proxy-from-env@1.1.0: {} - - pseudomap@1.0.2: {} + proxy-from-env@2.1.0: {} punycode@2.3.1: {} - pure-rand@6.1.0: {} - queue-microtask@1.2.3: {} quick-lru@4.0.1: {} - quick-lru@5.1.1: {} - - quote-unquote@1.0.0: {} - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - react-is@18.3.1: {} - read-cmd-shim@4.0.0: {} + react-is@19.2.8: {} - read-package-json-fast@3.0.2: - dependencies: - json-parse-even-better-errors: 3.0.2 - npm-normalize-package-bin: 3.0.1 + read-cmd-shim@4.0.0: {} - read-package-json@6.0.4: - dependencies: - glob: 10.4.5 - json-parse-even-better-errors: 3.0.2 - normalize-package-data: 5.0.0 - npm-normalize-package-bin: 3.0.1 + read-cmd-shim@5.0.0: {} read-pkg-up@3.0.0: dependencies: @@ -10519,13 +7831,9 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 - read@2.1.0: + read@4.1.0: dependencies: - mute-stream: 1.0.0 - - read@3.0.1: - dependencies: - mute-stream: 1.0.0 + mute-stream: 2.0.0 readable-stream@2.3.8: dependencies: @@ -10550,53 +7858,32 @@ snapshots: reflect.getprototypeof@1.0.10: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerator-runtime@0.14.1: {} - regexp.prototype.flags@1.5.4: dependencies: - call-bind: 1.0.8 + 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 - registry-auth-token@5.0.3: - dependencies: - '@pnpm/npm-conf': 2.3.1 - - registry-url@6.0.1: - dependencies: - rc: 1.2.8 - require-directory@2.1.1: {} require-from-string@2.0.2: {} - requirejs-config-file@4.0.0: - dependencies: - esprima: 4.0.1 - stringify-object: 3.3.0 - - requirejs@2.3.7: {} - - resolve-alpn@1.2.1: {} - resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 - resolve-dependency-path@3.0.2: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -10605,15 +7892,21 @@ snapshots: resolve.exports@2.0.3: {} - resolve@1.22.10: + resolve@1.22.12: dependencies: - is-core-module: 2.16.1 + es-errors: 1.3.0 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - responselike@3.0.0: + resolve@2.0.0-next.7: dependencies: - lowercase-keys: 3.0.0 + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 restore-cursor@3.1.0: dependencies: @@ -10632,7 +7925,7 @@ snapshots: retry@0.12.0: {} - reusify@1.0.4: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -10640,56 +7933,57 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@4.4.1: - dependencies: - glob: 9.3.5 - - rimraf@5.0.10: + rimraf@6.1.3: dependencies: - glob: 10.4.5 + glob: 13.0.6 + package-json-from-dist: 1.0.1 - rollup@4.31.0: + rollup@4.62.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.31.0 - '@rollup/rollup-android-arm64': 4.31.0 - '@rollup/rollup-darwin-arm64': 4.31.0 - '@rollup/rollup-darwin-x64': 4.31.0 - '@rollup/rollup-freebsd-arm64': 4.31.0 - '@rollup/rollup-freebsd-x64': 4.31.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.31.0 - '@rollup/rollup-linux-arm-musleabihf': 4.31.0 - '@rollup/rollup-linux-arm64-gnu': 4.31.0 - '@rollup/rollup-linux-arm64-musl': 4.31.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.31.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0 - '@rollup/rollup-linux-riscv64-gnu': 4.31.0 - '@rollup/rollup-linux-s390x-gnu': 4.31.0 - '@rollup/rollup-linux-x64-gnu': 4.31.0 - '@rollup/rollup-linux-x64-musl': 4.31.0 - '@rollup/rollup-win32-arm64-msvc': 4.31.0 - '@rollup/rollup-win32-ia32-msvc': 4.31.0 - '@rollup/rollup-win32-x64-msvc': 4.31.0 + '@rollup/rollup-android-arm-eabi': 4.62.3 + '@rollup/rollup-android-arm64': 4.62.3 + '@rollup/rollup-darwin-arm64': 4.62.3 + '@rollup/rollup-darwin-x64': 4.62.3 + '@rollup/rollup-freebsd-arm64': 4.62.3 + '@rollup/rollup-freebsd-x64': 4.62.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.3 + '@rollup/rollup-linux-arm-musleabihf': 4.62.3 + '@rollup/rollup-linux-arm64-gnu': 4.62.3 + '@rollup/rollup-linux-arm64-musl': 4.62.3 + '@rollup/rollup-linux-loong64-gnu': 4.62.3 + '@rollup/rollup-linux-loong64-musl': 4.62.3 + '@rollup/rollup-linux-ppc64-gnu': 4.62.3 + '@rollup/rollup-linux-ppc64-musl': 4.62.3 + '@rollup/rollup-linux-riscv64-gnu': 4.62.3 + '@rollup/rollup-linux-riscv64-musl': 4.62.3 + '@rollup/rollup-linux-s390x-gnu': 4.62.3 + '@rollup/rollup-linux-x64-gnu': 4.62.3 + '@rollup/rollup-linux-x64-musl': 4.62.3 + '@rollup/rollup-openbsd-x64': 4.62.3 + '@rollup/rollup-openharmony-arm64': 4.62.3 + '@rollup/rollup-win32-arm64-msvc': 4.62.3 + '@rollup/rollup-win32-ia32-msvc': 4.62.3 + '@rollup/rollup-win32-x64-gnu': 4.62.3 + '@rollup/rollup-win32-x64-msvc': 4.62.3 fsevents: 2.3.3 - run-async@2.4.1: {} - - run-async@3.0.0: {} + run-async@4.0.6: {} run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: + rxjs@7.8.2: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -10704,39 +7998,28 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 safer-buffer@2.1.2: {} - sass-lookup@5.0.1: - dependencies: - commander: 10.0.1 - - sembear@0.5.2: - dependencies: - '@types/semver': 6.2.7 - semver: 6.3.1 - semver@5.7.2: {} semver@6.3.1: {} - semver@7.5.3: - dependencies: - lru-cache: 6.0.0 + semver@7.7.2: {} - semver@7.6.3: {} + semver@7.7.4: {} - set-blocking@2.0.0: {} + 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.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -10751,49 +8034,39 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 + es-object-atoms: 1.1.2 shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 - object-inspect: 1.13.3 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 - object-inspect: 1.13.3 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 - side-channel-list: 1.0.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -10803,79 +8076,58 @@ snapshots: signal-exit@4.1.0: {} - sigstore@1.9.0: + sigstore@4.1.1: dependencies: - '@sigstore/bundle': 1.1.0 - '@sigstore/protobuf-specs': 0.2.1 - '@sigstore/sign': 1.0.0 - '@sigstore/tuf': 1.0.3 - make-fetch-happen: 11.1.1 + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.1 + '@sigstore/sign': 4.1.1 + '@sigstore/tuf': 4.0.2 + '@sigstore/verify': 3.1.1 transitivePeerDependencies: - supports-color - sisteransi@1.0.5: {} - slash@3.0.0: {} - slash@5.1.0: {} - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - slice-ansi@5.0.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 is-fullwidth-code-point: 4.0.0 smart-buffer@4.2.0: {} - socks-proxy-agent@7.0.0: + smol-toml@1.9.0: {} + + socks-proxy-agent@8.0.5: dependencies: - agent-base: 6.0.2 - debug: 4.4.0 - socks: 2.8.3 + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + socks: 2.8.9 transitivePeerDependencies: - supports-color - socks@2.8.3: + socks@2.8.9: dependencies: - ip-address: 9.0.5 + ip-address: 10.7.2 smart-buffer: 4.2.0 - sort-keys@2.0.0: - dependencies: - is-plain-obj: 1.1.0 - - source-map-js@1.2.1: {} - - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + source-map-js@1.2.2: {} source-map@0.6.1: {} - spawndamnit@2.0.0: - dependencies: - cross-spawn: 5.1.0 - signal-exit: 3.0.7 - spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.21 + spdx-license-ids: 3.0.23 spdx-exceptions@2.5.0: {} spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.21 + spdx-license-ids: 3.0.23 - spdx-license-ids@3.0.21: {} + spdx-license-ids@3.0.23: {} split2@3.2.2: dependencies: @@ -10885,41 +8137,29 @@ snapshots: dependencies: through: 2.3.8 - sprintf-js@1.0.3: {} - - sprintf-js@1.1.3: {} - - ssri@10.0.6: + ssri@12.0.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 - ssri@9.0.1: + ssri@13.0.1: dependencies: - minipass: 3.3.6 - - stable-hash@0.0.4: {} + minipass: 7.1.3 - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + stable-hash@0.0.5: {} stackback@0.0.2: {} - std-env@3.8.0: {} + std-env@4.2.0: {} - stdin-discarder@0.2.2: {} + stdin-discarder@0.3.2: {} - stream-to-array@2.3.0: + stop-iteration-iterator@1.1.0: dependencies: - any-promise: 1.3.0 + es-errors: 1.3.0 + internal-slot: 1.1.0 string-argv@0.3.2: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -10930,36 +8170,36 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 - string-width@7.2.0: + string-width@8.3.0: dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 - string.prototype.trim@1.2.10: + string.prototype.trim@1.2.11: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 + call-bind: 1.0.9 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.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.9: + string.prototype.trimend@1.0.10: dependencies: - call-bind: 1.0.8 - call-bound: 1.0.3 + call-bind: 1.0.9 + call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string_decoder@1.1.1: dependencies: @@ -10969,19 +8209,13 @@ snapshots: dependencies: safe-buffer: 5.2.1 - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.2.0: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.4.0 strip-bom@3.0.0: {} @@ -10995,105 +8229,33 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - strong-log-transformer@2.1.0: - dependencies: - duplexer: 0.1.2 - minimist: 1.2.8 - through: 2.3.8 - - stylus-lookup@5.0.1: - dependencies: - commander: 10.0.1 - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} - swagger-parser@10.0.3(openapi-types@12.1.3): - dependencies: - '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) - transitivePeerDependencies: - - openapi-types - - synckit@0.9.2: - dependencies: - '@pkgr/core': 0.1.1 - tslib: 2.8.1 - - table@6.9.0: + synckit@0.11.13: dependencies: - ajv: 8.17.1 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - tanu@0.1.13: - dependencies: - tslib: 2.8.1 - typescript: 4.9.5 - - tapable@2.2.1: {} + '@pkgr/core': 0.3.6 tar-stream@2.2.0: dependencies: bl: 4.1.0 - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 - tar@6.1.11: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 3.3.6 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - - tar@6.2.0: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - - temp-dir@1.0.0: {} - - temporal-polyfill@0.2.5: - dependencies: - temporal-spec: 0.2.4 - - temporal-spec@0.2.4: {} - - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - - test-exclude@7.0.1: + tar@7.5.21: dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.4.5 - minimatch: 9.0.5 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 text-extensions@1.9.0: {} @@ -11108,27 +8270,29 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.2: {} + tinyexec@1.3.1: {} - tinypool@1.0.2: {} - - tinyrainbow@2.0.0: {} - - tinyspy@3.0.2: {} + tinyglobby@0.2.12: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - tmp@0.0.33: + tinyglobby@0.2.17: dependencies: - os-tmpdir: 1.0.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - tmp@0.2.3: {} + tinyrainbow@3.1.1: {} - tmpl@1.0.5: {} + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - tr46@0.0.3: {} + tree-kill@1.2.2: {} + + treeverse@3.0.0: {} trim-newlines@3.0.1: {} @@ -11136,49 +8300,6 @@ snapshots: dependencies: typescript: 5.7.3 - ts-graphviz@1.8.2: {} - - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))(typescript@5.7.3): - dependencies: - bs-logger: 0.2.6 - ejs: 3.1.10 - fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - jest-util: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.6.3 - typescript: 5.7.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.26.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) - - ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.10.7 - acorn: 8.14.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.7.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - ts-pattern@5.6.2: {} - - ts-toolbelt@9.6.0: {} - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -11192,20 +8313,13 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@1.14.1: {} - tslib@2.8.1: {} - tsutils@3.21.0(typescript@5.7.3): + tuf-js@4.1.0: dependencies: - tslib: 1.14.1 - typescript: 5.7.3 - - tuf-js@1.1.7: - dependencies: - '@tufjs/models': 1.0.4 - debug: 4.4.0 - make-fetch-happen: 11.1.1 + '@tufjs/models': 4.1.0 + debug: 4.4.3(supports-color@7.2.0) + make-fetch-happen: 15.0.6 transitivePeerDependencies: - supports-color @@ -11213,61 +8327,50 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.0.8: {} - type-fest@0.18.1: {} type-fest@0.20.2: {} - type-fest@0.21.3: {} - - type-fest@0.4.1: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} type-fest@1.4.0: {} - type-fest@3.13.1: {} - typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + 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.8 - for-each: 0.3.3 + 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: + typed-array-byte-offset@1.0.5: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.3 + 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.7: + typed-array-length@1.0.8: dependencies: - call-bind: 1.0.8 - for-each: 0.3.3 + 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.0.0 + possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 typedarray@0.0.6: {} - typescript@4.9.5: {} - typescript@5.7.3: {} uglify-js@3.19.3: @@ -11275,44 +8378,49 @@ snapshots: unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.20.0: {} - - unicorn-magic@0.1.0: {} - - unique-filename@2.0.1: - dependencies: - unique-slug: 3.0.0 - - unique-filename@3.0.0: - dependencies: - unique-slug: 4.0.0 - - unique-slug@3.0.0: - dependencies: - imurmurhash: 0.1.4 + undici-types@6.21.0: {} - unique-slug@4.0.0: - dependencies: - imurmurhash: 0.1.4 + undici@6.29.0: {} universal-user-agent@6.0.1: {} - universalify@0.1.2: {} - universalify@2.0.1: {} + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + upath@2.0.1: {} - update-browserslist-db@1.1.2(browserslist@4.24.4): - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 + uri-js-replace@1.0.1: {} uri-js@4.4.1: dependencies: @@ -11320,168 +8428,83 @@ snapshots: util-deprecate@1.0.2: {} - uuid@9.0.1: {} - - v8-compile-cache-lib@3.0.1: {} - - v8-compile-cache@2.3.0: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - validate-npm-package-name@3.0.0: - dependencies: - builtins: 1.0.3 - - validate-npm-package-name@5.0.0: - dependencies: - builtins: 5.1.0 - - validator@13.12.0: {} + validate-npm-package-name@6.0.2: {} - verror@1.10.1: + vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1): dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.4.1 - - vite-node@3.0.3(@types/node@22.10.7)(yaml@2.7.0): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 2.0.2 - vite: 6.0.11(@types/node@22.10.7)(yaml@2.7.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@6.0.11(@types/node@22.10.7)(yaml@2.7.0): - dependencies: - esbuild: 0.24.2 - postcss: 8.5.1 - rollup: 4.31.0 + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.24 + rollup: 4.62.3 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 22.10.7 + '@types/node': 22.20.4 fsevents: 2.3.3 - yaml: 2.7.0 - - vitest@3.0.3(@types/node@22.10.7)(yaml@2.7.0): - dependencies: - '@vitest/expect': 3.0.3 - '@vitest/mocker': 3.0.3(vite@6.0.11(@types/node@22.10.7)(yaml@2.7.0)) - '@vitest/pretty-format': 3.0.3 - '@vitest/runner': 3.0.3 - '@vitest/snapshot': 3.0.3 - '@vitest/spy': 3.0.3 - '@vitest/utils': 3.0.3 - chai: 5.1.2 - debug: 4.4.0 - expect-type: 1.1.0 - magic-string: 0.30.17 - pathe: 2.0.2 - std-env: 3.8.0 + yaml: 2.9.1 + + vitest@4.1.11(@types/node@22.20.4)(@vitest/coverage-v8@4.1.11)(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@7.3.6(@types/node@22.20.4)(yaml@2.9.1)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.0.11(@types/node@22.10.7)(yaml@2.7.0) - vite-node: 3.0.3(@types/node@22.10.7)(yaml@2.7.0) + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@22.20.4)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.10.7 + '@types/node': 22.20.4 + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - walkdir@0.4.1: {} - walker@1.0.8: - dependencies: - makeerror: 1.0.12 + walk-up-path@4.0.0: {} wcwidth@1.0.1: dependencies: defaults: 1.0.4 - webidl-conversions@3.0.1: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - whence@2.0.1: - dependencies: - '@babel/parser': 7.26.5 - eval-estree-expression: 2.0.3 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 - is-boolean-object: 1.2.1 + 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.3 - function.prototype.name: 1.1.8 + call-bound: 1.0.4 + function.prototype.name: 1.2.0 has-tostringtag: 1.0.2 - is-async-function: 2.1.0 + is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 - is-weakref: 1.1.0 + 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.18 + which-typed-array: 1.1.24 which-collection@1.0.2: dependencies: @@ -11490,26 +8513,23 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: + which-typed-array@1.1.24: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.3 - for-each: 0.3.3 + 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@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 - which@3.0.1: + which@6.0.1: dependencies: - isexe: 2.0.0 + isexe: 4.0.0 why-is-node-running@2.3.0: dependencies: @@ -11538,68 +8558,41 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 wrappy@1.0.2: {} - write-file-atomic@2.4.3: - dependencies: - graceful-fs: 4.2.11 - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4 signal-exit: 4.1.0 - write-json-file@3.2.0: + write-file-atomic@6.0.0: dependencies: - detect-indent: 5.0.0 - graceful-fs: 4.2.11 - make-dir: 2.1.0 - pify: 4.0.1 - sort-keys: 2.0.0 - write-file-atomic: 2.4.3 - - write-pkg@4.0.0: - dependencies: - sort-keys: 2.0.0 - type-fest: 0.4.1 - write-json-file: 3.2.0 + imurmurhash: 0.1.4 + signal-exit: 4.1.0 xtend@4.0.2: {} y18n@5.0.8: {} - yallist@2.1.2: {} - - yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@1.10.2: {} - - yaml@2.3.1: {} + yallist@5.0.0: {} - yaml@2.5.1: {} + yaml-ast-parser@0.0.43: {} - yaml@2.7.0: {} + yaml@2.9.0: {} - yargs-parser@20.2.4: {} + yaml@2.9.1: {} yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} - yargs@16.2.0: + yargs@16.2.2: dependencies: cliui: 7.0.4 escalade: 3.2.0 @@ -11619,18 +8612,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yn@3.1.1: {} - yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} - - z-schema@5.0.5: - dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.12.0 - optionalDependencies: - commander: 9.5.0 + yoctocolors-cjs@2.1.3: {} - zod@3.24.1: {} + yoctocolors@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 281197a..dee51e9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,2 @@ packages: - "packages/*" - - "packages/generators/*" - - "packages/providers/*" - - "packages/types/*" diff --git a/spec-graduate.gif b/spec-graduate.gif deleted file mode 100644 index 3a040a5..0000000 Binary files a/spec-graduate.gif and /dev/null differ diff --git a/tests/e2e/01-init.test.ts b/tests/e2e/01-init.test.ts new file mode 100644 index 0000000..5ba98ae --- /dev/null +++ b/tests/e2e/01-init.test.ts @@ -0,0 +1,133 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + fileExists, + readYAML, + readJSON, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual init', () => { + test('detects OpenAPI spec and scaffolds config', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.openapi.yaml')); + + const result = run('init', dir); + + // Assert: config created + expect(fileExists(dir, 'contractual.yaml')).toBe(true); + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array<{ type: string; path: string }> }; + expect(config.contracts).toHaveLength(1); + expect(config.contracts[0].type).toBe('openapi'); + expect(config.contracts[0].path).toContain('api.openapi.yaml'); + + // Assert: .contractual directory created + expect(fileExists(dir, '.contractual/versions.json')).toBe(true); + expect(fileExists(dir, '.contractual/changesets')).toBe(true); + expect(fileExists(dir, '.contractual/snapshots')).toBe(true); + + // Assert: versions.json is populated with initial version + const versions = readJSON(dir, '.contractual/versions.json') as Record; + const contractName = Object.keys(versions)[0]; + expect(contractName).toBeDefined(); + expect(versions[contractName].version).toBe('0.0.0'); + + // Assert: stdout confirms detection + expect(result.stdout).toMatch(/found|detected|initialized/i); + } finally { + cleanup(); + } + }); + + test('detects JSON Schema by extension', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.schema.json')); + + run('init', dir); + + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array<{ type: string }> }; + expect(config.contracts).toHaveLength(1); + expect(config.contracts[0].type).toBe('json-schema'); + } finally { + cleanup(); + } + }); + + test('detects multiple spec types in one repo', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.openapi.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.schema.json')); + + run('init', dir); + + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array<{ type: string }> }; + expect(config.contracts).toHaveLength(2); + + const types = config.contracts.map((c) => c.type).sort(); + expect(types).toEqual(['json-schema', 'openapi']); + } finally { + cleanup(); + } + }); + + test('handles already initialized gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.openapi.yaml')); + + run('init', dir); + + // Second init should succeed with informational message (use --force to reinitialize) + const result = run('init', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout + result.stderr).toMatch(/already initialized|all contracts have snapshots/i); + } finally { + cleanup(); + } + }); + + test('handles repo with no specs', () => { + const { dir, cleanup } = createTempRepo(); + try { + const result = run('init', dir); + + // Should still complete (maybe with a warning) + expect(result.exitCode).toBe(0); + + // Config may or may not be created with empty contracts + if (fileExists(dir, 'contractual.yaml')) { + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array }; + expect(config.contracts).toHaveLength(0); + } + } finally { + cleanup(); + } + }); + + test('uses parent directory name for generic spec names', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Use "openapi.yaml" (generic name) in "orders" directory + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'orders/openapi.yaml')); + + run('init', dir); + + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array<{ name: string }> }; + expect(config.contracts).toHaveLength(1); + // Should use "orders" as the name, not "openapi" + expect(config.contracts[0].name).toBe('orders'); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/02-lint.test.ts b/tests/e2e/02-lint.test.ts new file mode 100644 index 0000000..993fabe --- /dev/null +++ b/tests/e2e/02-lint.test.ts @@ -0,0 +1,192 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual lint', () => { + // Note: No built-in linters are registered yet in linterRegistry. + // All contracts are skipped with "no linter available for {type}" message. + // These tests verify the CLI handles this gracefully (exit 0, no errors). + + test('OpenAPI spec linted with Spectral returns errors for petstore fixture', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // OpenAPI linter (Spectral) is registered and finds errors in petstore fixture + const result = run('lint --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + const parsed = JSON.parse(result.stdout); + expect(parsed.results).toHaveLength(1); + expect(parsed.results[0].errors.length).toBeGreaterThan(0); + } finally { + cleanup(); + } + }); + + test('JSON Schema without registered linter exits successfully', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // No linter registered = contract is skipped, exit 0 + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + // Skip: No linters are registered, so invalid schemas cannot be validated + test.skip('lint reports errors for invalid JSON Schema and exits 1', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'bad-schema', + type: 'json-schema', + path: 'schemas/bad.json', + }, + ]); + // Write an intentionally invalid JSON Schema + writeFile( + dir, + 'schemas/bad.json', + JSON.stringify({ + type: 'objekt', // typo - invalid type value + properties: { + id: { type: 123 }, // type should be string, not number + }, + }) + ); + + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/error/i); + } finally { + cleanup(); + } + }); + + test('lint --format json outputs valid JSON', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('lint --format json', dir); + const parsed = JSON.parse(result.stdout); + expect(parsed).toHaveProperty('results'); + expect(Array.isArray(parsed.results)).toBe(true); + } finally { + cleanup(); + } + }); + + test('lint with disabled linter skips contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + lint: false, + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // With --format json, disabled contracts are not included in results + const result = run('lint --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + // Results should be empty since contract is disabled + expect(parsed.results).toHaveLength(0); + } finally { + cleanup(); + } + }); + + test('lint --contract filters to single contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + { name: 'schema', type: 'json-schema', path: 'schemas/order.json' }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // OpenAPI linter runs for 'api' contract, petstore has lint errors + const result = run('lint --contract api --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + const parsed = JSON.parse(result.stdout); + // Should have exactly 1 result (the filtered 'api' contract) + expect(parsed.results).toHaveLength(1); + expect(parsed.results[0].contract).toBe('api'); + } finally { + cleanup(); + } + }); + + test('lint reports error for non-existent contract name', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + const result = run('lint --contract nonexistent', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/not found/i); + } finally { + cleanup(); + } + }); + + test('lint fails with empty contracts array due to schema validation', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Config schema requires minItems: 1 for contracts array + setupRepoWithConfig(dir, []); + + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + // Should fail config validation + expect(result.stdout + result.stderr).toMatch(/contracts|validation|invalid/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/03-breaking.test.ts b/tests/e2e/03-breaking.test.ts new file mode 100644 index 0000000..bfff362 --- /dev/null +++ b/tests/e2e/03-breaking.test.ts @@ -0,0 +1,274 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual breaking', () => { + test('no snapshot β†’ reports first version, no breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + // First version with no snapshot shows "No changes detected" + expect(result.stdout).toMatch(/no changes detected/i); + } finally { + cleanup(); + } + }); + + test('detects breaking change in JSON Schema (field removed)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + // Put base as snapshot (simulating a previous release) + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + // Put breaking variant as current spec + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('detects non-breaking change (optional field added)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + + const result = run('breaking', dir); + // Non-breaking = exit 0 (only breaking exits 1) + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/non-breaking|minor|no breaking/i); + } finally { + cleanup(); + } + }); + + test('no changes detected when spec is identical', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no.*change|no breaking/i); + } finally { + cleanup(); + } + }); + + test('--format json returns structured output', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking --format json', dir, { expectFail: true }); + const parsed = JSON.parse(result.stdout); + expect(parsed).toHaveProperty('results'); + expect(parsed).toHaveProperty('hasBreaking'); + expect(parsed.hasBreaking).toBe(true); + expect(Array.isArray(parsed.results)).toBe(true); + expect(parsed.results[0]).toHaveProperty('changes'); + expect(parsed.results[0]).toHaveProperty('summary'); + } finally { + cleanup(); + } + }); + + test('detects type change as breaking', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture('json-schema/order-type-changed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking|type/i); + } finally { + cleanup(); + } + }); + + test('--contract filters to single contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + // Set up snapshots for both + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/user-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Breaking change in order, no change in user + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Check only user-schema - should pass + const result = run('breaking --contract user-schema', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('breaking detection disabled for contract skips it', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + breaking: false, + }, + ]); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + // Disabled contracts show "No changes detected" + expect(result.stdout).toMatch(/no changes detected/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/04-changeset.test.ts b/tests/e2e/04-changeset.test.ts new file mode 100644 index 0000000..cbd87c4 --- /dev/null +++ b/tests/e2e/04-changeset.test.ts @@ -0,0 +1,362 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual changeset', () => { + test('auto-generates changeset from detected breaking changes (major bump)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Put base as snapshot (simulating a previous release) + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Put breaking variant as current spec (field removed) + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // A changeset file should exist + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThanOrEqual(1); + + // Parse the changeset + const content = readFile(dir, `.contractual/changesets/${changesets[0]}`); + + // Frontmatter should have major bump + expect(content).toMatch(/"order-schema":\s*major/); + + // Body should contain the contract name section + expect(content).toMatch(/##\s*order-schema/); + + // Body should mention BREAKING + expect(content).toMatch(/\*\*\[BREAKING\]\*\*/); + } finally { + cleanup(); + } + }); + + test('auto-generates minor changeset for non-breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Put base as snapshot + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Put non-breaking variant as current spec (optional field added) + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThanOrEqual(1); + + const content = readFile(dir, `.contractual/changesets/${changesets[0]}`); + + // Frontmatter should have minor bump (not major) + expect(content).toMatch(/"order-schema":\s*minor/); + + // Body should contain the contract section + expect(content).toMatch(/##\s*order-schema/); + + // Body should mention minor or non-breaking change + expect(content).toMatch(/\*\*\[minor\]\*\*/); + } finally { + cleanup(); + } + }); + + test('no changes -> no changeset created, clean exit', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Put base as snapshot + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + // Put identical spec as current + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // Output should indicate no changes + expect(result.stdout).toMatch(/no changes/i); + + // No changeset files should be created (except .gitkeep if present) + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets).toHaveLength(0); + } finally { + cleanup(); + } + }); + + test('changeset for multiple contracts in one file', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + // Both have snapshots at v1 (using order-base as template for both) + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/user-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Breaking change to order (field removed), non-breaking to user (optional field added) + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/user.json') + ); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThanOrEqual(1); + + const content = readFile(dir, `.contractual/changesets/${changesets[0]}`); + + // Should have both contracts in the frontmatter + expect(content).toMatch(/"order-schema":\s*major/); + expect(content).toMatch(/"user-schema":\s*minor/); + + // Should have sections for both contracts + expect(content).toMatch(/##\s*order-schema/); + expect(content).toMatch(/##\s*user-schema/); + } finally { + cleanup(); + } + }); + + test('first version contract (no snapshot) is skipped', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // No snapshot exists - first version + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Empty versions + writeFile(dir, '.contractual/versions.json', '{}'); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // No changes to detect without a snapshot + expect(result.stdout).toMatch(/no changes/i); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets).toHaveLength(0); + } finally { + cleanup(); + } + }); + + test('changeset with type change (breaking)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Type changed (breaking) + copyFixture('json-schema/order-type-changed.json', path.join(dir, 'schemas/order.json')); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets.length).toBeGreaterThanOrEqual(1); + + const content = readFile(dir, `.contractual/changesets/${changesets[0]}`); + + // Should be major bump + expect(content).toMatch(/"order-schema":\s*major/); + } finally { + cleanup(); + } + }); + + test('contract with breaking detection disabled is skipped', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + breaking: false, + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Breaking change that would normally be detected + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // No changeset should be created since breaking detection is disabled + expect(result.stdout).toMatch(/no changes/i); + + const changesets = listFiles(dir, '.contractual/changesets').filter((f) => f.endsWith('.md')); + expect(changesets).toHaveLength(0); + } finally { + cleanup(); + } + }); + + test('changeset output indicates created file path', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // Output should mention the changeset file was created + expect(result.stdout).toMatch(/created.*changeset/i); + expect(result.stdout).toMatch(/\.contractual\/changesets/); + expect(result.stdout).toMatch(/\.md/); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/05-version.test.ts b/tests/e2e/05-version.test.ts new file mode 100644 index 0000000..90b40a6 --- /dev/null +++ b/tests/e2e/05-version.test.ts @@ -0,0 +1,484 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + listFiles, + fileExists, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual version', () => { + test('consumes changeset, bumps version, updates snapshot, writes changelog', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Setup repo with a contract + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Set initial version + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a changeset + writeFile( + dir, + '.contractual/changesets/add-notes-field.md', + `--- +"order-schema": minor +--- +## order-schema +- Added optional notes field to order +` + ); + + const result = run('version', dir); + + // Assert: command succeeds + expect(result.exitCode).toBe(0); + + // Assert: version bumped to 1.1.0 (minor bump from 1.0.0) + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string; released: string } + >; + expect(versions['order-schema'].version).toBe('1.1.0'); + expect(versions['order-schema'].released).toBeDefined(); + + // Assert: snapshot created/updated + expect(fileExists(dir, '.contractual/snapshots/order-schema.json')).toBe(true); + + // Assert: changelog created with entry + expect(fileExists(dir, 'CHANGELOG.md')).toBe(true); + const changelog = readFile(dir, 'CHANGELOG.md'); + expect(changelog).toContain('# Changelog'); + expect(changelog).toContain('[order-schema] v1.1.0'); + expect(changelog).toContain('Added optional notes field'); + + // Assert: changeset file removed + const changesetFiles = listFiles(dir, '.contractual/changesets'); + expect(changesetFiles).not.toContain('add-notes-field.md'); + + // Assert: stdout confirms version bump + expect(result.stdout).toMatch(/1\.0\.0.*->.*1\.1\.0/); + } finally { + cleanup(); + } + }); + + test('multiple changesets: highest bump wins (major > minor > patch)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'api-schema', + type: 'json-schema', + path: 'schemas/api.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/api.json')); + + // Set initial version + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'api-schema': { version: '2.5.3', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create multiple changesets with different bump types + // Changeset 1: patch bump + writeFile( + dir, + '.contractual/changesets/fix-typo.md', + `--- +"api-schema": patch +--- +## api-schema +- Fixed typo in description +` + ); + + // Changeset 2: minor bump + writeFile( + dir, + '.contractual/changesets/add-field.md', + `--- +"api-schema": minor +--- +## api-schema +- Added new optional field +` + ); + + // Changeset 3: major bump (should win) + writeFile( + dir, + '.contractual/changesets/breaking-change.md', + `--- +"api-schema": major +--- +## api-schema +- Removed deprecated endpoint (BREAKING) +` + ); + + const result = run('version', dir); + + // Assert: command succeeds + expect(result.exitCode).toBe(0); + + // Assert: major bump applied (2.5.3 -> 3.0.0) + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api-schema'].version).toBe('3.0.0'); + + // Assert: all changesets consumed + const changesetFiles = listFiles(dir, '.contractual/changesets'); + expect(changesetFiles).toHaveLength(0); + + // Assert: changelog contains all changes + const changelog = readFile(dir, 'CHANGELOG.md'); + expect(changelog).toContain('v3.0.0'); + expect(changelog).toContain('Fixed typo'); + expect(changelog).toContain('Added new optional field'); + expect(changelog).toContain('Removed deprecated endpoint'); + } finally { + cleanup(); + } + }); + + test('no changesets does nothing', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Set version but no changesets + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('version', dir); + + // Assert: command succeeds (no-op) + expect(result.exitCode).toBe(0); + + // Assert: version unchanged + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.0.0'); + + // Assert: no changelog created + expect(fileExists(dir, 'CHANGELOG.md')).toBe(false); + + // Assert: stdout indicates nothing to do + expect(result.stdout).toMatch(/no.*changeset|nothing/i); + } finally { + cleanup(); + } + }); + + test('first version: 0.0.0 -> 1.0.0 on major', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'new-api', + type: 'json-schema', + path: 'schemas/new-api.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/new-api.json')); + + // No initial version (first release) + // versions.json is empty (setup creates empty object) + + // Create changeset with major bump + writeFile( + dir, + '.contractual/changesets/initial-release.md', + `--- +"new-api": major +--- +## new-api +- Initial release of the API +` + ); + + const result = run('version', dir); + + // Assert: command succeeds + expect(result.exitCode).toBe(0); + + // Assert: version is 1.0.0 (major bump from implicit 0.0.0) + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['new-api'].version).toBe('1.0.0'); + + // Assert: snapshot created + expect(fileExists(dir, '.contractual/snapshots/new-api.json')).toBe(true); + + // Assert: changelog created + expect(fileExists(dir, 'CHANGELOG.md')).toBe(true); + const changelog = readFile(dir, 'CHANGELOG.md'); + expect(changelog).toContain('[new-api] v1.0.0'); + expect(changelog).toContain('Initial release'); + + // Assert: stdout shows bump from 0.0.0 + expect(result.stdout).toMatch(/0\.0\.0.*->.*1\.0\.0/); + } finally { + cleanup(); + } + }); + + test('changelog appends, does not overwrite existing entries', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'api', + type: 'json-schema', + path: 'schemas/api.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/api.json')); + + // Set initial version + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create existing changelog with previous entries + writeFile( + dir, + 'CHANGELOG.md', + `# Changelog + +## [api] v1.0.0 - 2026-01-01 + +- Initial release with core features +- Added authentication support +` + ); + + // Create a new changeset + writeFile( + dir, + '.contractual/changesets/add-feature.md', + `--- +"api": minor +--- +## api +- Added pagination support +` + ); + + const result = run('version', dir); + + // Assert: command succeeds + expect(result.exitCode).toBe(0); + + // Assert: version bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api'].version).toBe('1.1.0'); + + // Assert: changelog preserved old entries AND has new entry + const changelog = readFile(dir, 'CHANGELOG.md'); + + // New entry should be present + expect(changelog).toContain('[api] v1.1.0'); + expect(changelog).toContain('Added pagination support'); + + // Old entries should still be present + expect(changelog).toContain('[api] v1.0.0'); + expect(changelog).toContain('Initial release with core features'); + expect(changelog).toContain('Added authentication support'); + + // New entry should come before old entry (prepended) + const v110Index = changelog.indexOf('[api] v1.1.0'); + const v100Index = changelog.indexOf('[api] v1.0.0'); + expect(v110Index).toBeLessThan(v100Index); + } finally { + cleanup(); + } + }); + + test('handles multiple contracts in one changeset', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'orders-api', + type: 'json-schema', + path: 'schemas/orders.json', + }, + { + name: 'users-api', + type: 'json-schema', + path: 'schemas/users.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/orders.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/users.json')); + + // Set initial versions + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'orders-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'users-api': { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create changeset affecting both contracts + writeFile( + dir, + '.contractual/changesets/shared-update.md', + `--- +"orders-api": minor +"users-api": patch +--- +## orders-api +- Added bulk operations endpoint + +## users-api +- Fixed validation message +` + ); + + const result = run('version', dir); + + // Assert: command succeeds + expect(result.exitCode).toBe(0); + + // Assert: both versions bumped appropriately + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['orders-api'].version).toBe('1.1.0'); + expect(versions['users-api'].version).toBe('2.0.1'); + + // Assert: both snapshots created + expect(fileExists(dir, '.contractual/snapshots/orders-api.json')).toBe(true); + expect(fileExists(dir, '.contractual/snapshots/users-api.json')).toBe(true); + + // Assert: changelog has entries for both + const changelog = readFile(dir, 'CHANGELOG.md'); + expect(changelog).toContain('[orders-api] v1.1.0'); + expect(changelog).toContain('[users-api] v2.0.1'); + expect(changelog).toContain('Added bulk operations endpoint'); + expect(changelog).toContain('Fixed validation message'); + } finally { + cleanup(); + } + }); + + test('preserves all changesets and versions when a contract is unknown', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'existing-api', + type: 'json-schema', + path: 'schemas/existing.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/existing.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'existing-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create changeset referencing a non-existent contract + writeFile( + dir, + '.contractual/changesets/mixed-update.md', + `--- +"existing-api": minor +"nonexistent-api": major +--- +## existing-api +- Updated existing API + +## nonexistent-api +- This contract does not exist +` + ); + + const result = run('version', dir, { expectFail: true }); + + // Validation happens before any version or changeset mutation. + expect(result.exitCode).not.toBe(0); + + // Assert: existing contract was not bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['existing-api'].version).toBe('1.0.0'); + + // Assert: nonexistent contract was not added to versions + expect(versions['nonexistent-api']).toBeUndefined(); + + // Assert: changeset was preserved + const changesetFiles = listFiles(dir, '.contractual/changesets'); + expect(changesetFiles).toEqual(['mixed-update.md']); + + // Assert: no changelog was created + expect(fileExists(dir, 'CHANGELOG.md')).toBe(false); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/06-status.test.ts b/tests/e2e/06-status.test.ts new file mode 100644 index 0000000..38c1ba2 --- /dev/null +++ b/tests/e2e/06-status.test.ts @@ -0,0 +1,250 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual status', () => { + test('shows current versions from versions.json', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-schema', + type: 'json-schema', + path: 'schemas/user.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Set up versions + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.2.3', released: '2026-01-15T10:00:00Z' }, + 'user-schema': { version: '2.0.0', released: '2026-02-01T12:00:00Z' }, + }) + ); + + const result = run('status', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).toMatch(/1\.2\.3/); + expect(result.stdout).toMatch(/user-schema/); + expect(result.stdout).toMatch(/2\.0\.0/); + } finally { + cleanup(); + } + }); + + test('shows pending changesets', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Set up version + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a pending changeset + const changesetContent = `--- +"order-schema": minor +--- + +## order-schema + +- **[minor]** Added optional tracking_number field +`; + writeFile(dir, '.contractual/changesets/friendly-tiger.md', changesetContent); + + const result = run('status', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/pending/i); + expect(result.stdout).toMatch(/friendly-tiger\.md/); + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).toMatch(/minor/i); + } finally { + cleanup(); + } + }); + + test('shows projected bumps based on pending changesets', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-schema', + type: 'json-schema', + path: 'schemas/user.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Set up versions + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.5.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a changeset with major bump for order-schema + const changesetContent = `--- +"order-schema": major +"user-schema": patch +--- + +## order-schema + +- **[BREAKING]** Removed customer_email field + +## user-schema + +- **[patch]** Updated description +`; + writeFile(dir, '.contractual/changesets/breaking-change.md', changesetContent); + + const result = run('status', dir); + + expect(result.exitCode).toBe(0); + // Should show projected version 2.0.0 for order-schema (major bump from 1.0.0) + expect(result.stdout).toMatch(/2\.0\.0/); + // Should show projected version 1.5.1 for user-schema (patch bump from 1.5.0) + expect(result.stdout).toMatch(/1\.5\.1/); + // Should indicate major and patch bumps + expect(result.stdout).toMatch(/major/i); + expect(result.stdout).toMatch(/patch/i); + } finally { + cleanup(); + } + }); + + test('handles empty state (no versions, no changesets)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // versions.json is empty (created by setupRepoWithConfig) + const result = run('status', dir); + + expect(result.exitCode).toBe(0); + // Should show 0.0.0 or unreleased + expect(result.stdout).toMatch(/0\.0\.0|unreleased/i); + // Should indicate no pending changesets + expect(result.stdout).toMatch(/no pending changeset/i); + } finally { + cleanup(); + } + }); + + test('aggregates multiple changesets correctly (highest bump wins)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Set up version + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create multiple changesets - patch and minor for same contract + const patchChangeset = `--- +"order-schema": patch +--- + +Fixed typo in description +`; + const minorChangeset = `--- +"order-schema": minor +--- + +Added optional field +`; + writeFile(dir, '.contractual/changesets/fix-typo.md', patchChangeset); + writeFile(dir, '.contractual/changesets/add-field.md', minorChangeset); + + const result = run('status', dir); + + expect(result.exitCode).toBe(0); + // Should show 2 pending changesets + expect(result.stdout).toMatch(/2.*changeset/i); + // Should project to 1.1.0 (minor wins over patch) + expect(result.stdout).toMatch(/1\.1\.0/); + } finally { + cleanup(); + } + }); + + test('reports error when not initialized', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Don't run init or setupRepoWithConfig - no .contractual directory + + const result = run('status', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + // Error message contains "No contractual.yaml found" + expect(result.stdout + result.stderr).toMatch(/contractual\.yaml|init/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/07-full-lifecycle.test.ts b/tests/e2e/07-full-lifecycle.test.ts new file mode 100644 index 0000000..f7844a0 --- /dev/null +++ b/tests/e2e/07-full-lifecycle.test.ts @@ -0,0 +1,372 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + fileExists, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual full lifecycle', () => { + test('complete cycle: init -> first release -> changes -> changeset -> second release', () => { + const { dir, cleanup } = createTempRepo(); + try { + // ===== PHASE 1: Setup with a spec using setupRepoWithConfig ===== + setupRepoWithConfig(dir, [ + { + name: 'order', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // ===== PHASE 2: Create initial changeset manually, run version (1.0.0) ===== + const initialChangeset = `--- +"order": major +--- + +## order + +- **[major]** Initial release of order schema +`; + writeFile(dir, '.contractual/changesets/initial-release.md', initialChangeset); + + // Run version to consume the changeset + const versionResult1 = run('version', dir); + expect(versionResult1.exitCode).toBe(0); + expect(versionResult1.stdout).toMatch(/1\.0\.0/); + + // Verify versions.json was updated + const versions1 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions1['order']).toBeDefined(); + expect(versions1['order'].version).toBe('1.0.0'); + + // Verify changeset was consumed + const changesetsAfterV1 = listFiles(dir, '.contractual/changesets'); + expect(changesetsAfterV1.filter((f) => f.endsWith('.md'))).toHaveLength(0); + + // Verify snapshot was created + expect(fileExists(dir, '.contractual/snapshots/order.json')).toBe(true); + + // Verify CHANGELOG.md was created + expect(fileExists(dir, 'CHANGELOG.md')).toBe(true); + const changelog1 = readFile(dir, 'CHANGELOG.md'); + expect(changelog1).toMatch(/1\.0\.0/); + + // ===== PHASE 3: Make breaking change to spec ===== + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + // ===== PHASE 4: Run changeset command (auto-detect) ===== + const changesetResult = run('changeset', dir); + expect(changesetResult.exitCode).toBe(0); + expect(changesetResult.stdout).toMatch(/created changeset/i); + + // Verify a changeset file was created + const changesetsAfterDetect = listFiles(dir, '.contractual/changesets'); + const mdFiles = changesetsAfterDetect.filter((f) => f.endsWith('.md')); + expect(mdFiles.length).toBeGreaterThan(0); + + // Verify the changeset contains major bump (breaking change) + const changesetFile = readFile(dir, `.contractual/changesets/${mdFiles[0]}`); + expect(changesetFile).toMatch(/major/i); + + // ===== PHASE 5: Run status (shows projected bump) ===== + const statusResult = run('status', dir); + expect(statusResult.exitCode).toBe(0); + expect(statusResult.stdout).toMatch(/1\.0\.0/); // Current version + expect(statusResult.stdout).toMatch(/2\.0\.0/); // Projected version + expect(statusResult.stdout).toMatch(/major/i); + expect(statusResult.stdout).toMatch(/pending/i); + + // ===== PHASE 6: Run version (bumps to 2.0.0) ===== + const versionResult2 = run('version', dir); + expect(versionResult2.exitCode).toBe(0); + expect(versionResult2.stdout).toMatch(/2\.0\.0/); + + // Verify versions.json was updated + const versions2 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions2['order'].version).toBe('2.0.0'); + + // Verify CHANGELOG.md was updated + const changelog2 = readFile(dir, 'CHANGELOG.md'); + expect(changelog2).toMatch(/2\.0\.0/); + expect(changelog2).toMatch(/1\.0\.0/); // Previous version still there + + // ===== PHASE 7: Verify breaking shows no changes after version ===== + const breakingResult = run('breaking', dir); + expect(breakingResult.exitCode).toBe(0); + expect(breakingResult.stdout).toMatch(/no.*change|no breaking/i); + } finally { + cleanup(); + } + }); + + test('multiple releases accumulate in changelog (1.0.0 -> 1.1.0 -> 1.1.1)', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Setup initial state + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Create snapshot (simulating a previous version) + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + // ===== Release 1.0.0 ===== + const release1Changeset = `--- +"order-schema": major +--- + +## order-schema + +- **[major]** Initial release +`; + writeFile(dir, '.contractual/changesets/release-1.md', release1Changeset); + + const version1 = run('version', dir); + expect(version1.exitCode).toBe(0); + + const versions1 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions1['order-schema'].version).toBe('1.0.0'); + + // ===== Release 1.1.0 (minor bump - add optional field) ===== + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + + // Auto-detect changes + const changeset2 = run('changeset', dir); + expect(changeset2.exitCode).toBe(0); + + const version2 = run('version', dir); + expect(version2.exitCode).toBe(0); + + const versions2 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions2['order-schema'].version).toBe('1.1.0'); + + // ===== Release 1.1.1 (patch bump - description change) ===== + copyFixture( + 'json-schema/order-description-changed.json', + path.join(dir, 'schemas/order.json') + ); + + // Create manual patch changeset + const patchChangeset = `--- +"order-schema": patch +--- + +## order-schema + +- **[patch]** Updated description text +`; + writeFile(dir, '.contractual/changesets/patch-desc.md', patchChangeset); + + const version3 = run('version', dir); + expect(version3.exitCode).toBe(0); + + const versions3 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions3['order-schema'].version).toBe('1.1.1'); + + // ===== Verify changelog has all three versions ===== + const changelog = readFile(dir, 'CHANGELOG.md'); + expect(changelog).toMatch(/1\.0\.0/); + expect(changelog).toMatch(/1\.1\.0/); + expect(changelog).toMatch(/1\.1\.1/); + + // Verify order in changelog (newest first typically) + const v100Index = changelog.indexOf('1.0.0'); + const v110Index = changelog.indexOf('1.1.0'); + const v111Index = changelog.indexOf('1.1.1'); + + // Newest versions should appear first in changelog + expect(v111Index).toBeLessThan(v110Index); + expect(v110Index).toBeLessThan(v100Index); + } finally { + cleanup(); + } + }); + + test('handles multiple contracts with independent versioning', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-schema', + type: 'json-schema', + path: 'schemas/user.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // ===== Release both contracts at 1.0.0 ===== + const initialChangeset = `--- +"order-schema": major +"user-schema": major +--- + +Initial release of both schemas +`; + writeFile(dir, '.contractual/changesets/initial.md', initialChangeset); + + const version1 = run('version', dir); + expect(version1.exitCode).toBe(0); + + const versions1 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions1['order-schema'].version).toBe('1.0.0'); + expect(versions1['user-schema'].version).toBe('1.0.0'); + + // ===== Bump only order-schema to 2.0.0 ===== + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const orderChangeset = `--- +"order-schema": major +--- + +## order-schema + +- **[BREAKING]** Removed customer_email field +`; + writeFile(dir, '.contractual/changesets/order-breaking.md', orderChangeset); + + const version2 = run('version', dir); + expect(version2.exitCode).toBe(0); + + const versions2 = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions2['order-schema'].version).toBe('2.0.0'); + expect(versions2['user-schema'].version).toBe('1.0.0'); // Unchanged + + // ===== Verify status shows correct versions ===== + const status = run('status', dir); + expect(status.exitCode).toBe(0); + expect(status.stdout).toMatch(/order-schema/); + expect(status.stdout).toMatch(/2\.0\.0/); + expect(status.stdout).toMatch(/user-schema/); + } finally { + cleanup(); + } + }); + + test('version command with no changesets does nothing', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // No changesets - just run version + const result = run('version', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no pending|nothing to version/i); + + // Version should remain unchanged + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + + test('changeset command with no changes does nothing', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Create snapshot matching current spec + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Spec matches snapshot - no changes + const result = run('changeset', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no change/i); + + // No changeset should be created + const changesets = listFiles(dir, '.contractual/changesets'); + expect(changesets.filter((f) => f.endsWith('.md'))).toHaveLength(0); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/08-multi-contract.test.ts b/tests/e2e/08-multi-contract.test.ts new file mode 100644 index 0000000..da31b53 --- /dev/null +++ b/tests/e2e/08-multi-contract.test.ts @@ -0,0 +1,337 @@ +import { describe, test, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + fileExists, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('multi-contract scenarios', () => { + let tempRepo: { dir: string; cleanup: () => void }; + + beforeEach(() => { + tempRepo = createTempRepo(); + }); + + afterEach(() => { + tempRepo.cleanup(); + }); + + test('lint works with multiple contracts of different types', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { name: 'petstore-api', type: 'openapi', path: 'specs/petstore.yaml' }, + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // OpenAPI linter (Spectral) runs and finds errors in petstore fixture + // JSON Schema linter validates and passes for valid schemas + const result = run('lint --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); // petstore has lint errors + + const parsed = JSON.parse(result.stdout); + // All 3 contracts have results + expect(parsed.results).toHaveLength(3); + // petstore has errors, JSON schemas pass + const petstoreResult = parsed.results.find((r: { contract: string }) => r.contract === 'petstore-api'); + expect(petstoreResult.errors.length).toBeGreaterThan(0); + }); + + test('breaking detects changes in multiple contracts', () => { + const { dir } = tempRepo; + + // Use only json-schema contracts for reliable testing + // (openapi requires oasdiff binary which may not be available) + setupRepoWithConfig(dir, [ + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + // Set up snapshots for json-schema contracts + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/user-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current specs: breaking change in order-schema, no change in user + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + expect(parsed.results).toHaveLength(2); + + // Find results by contract name + const orderResult = parsed.results.find( + (r: { contract: string }) => r.contract === 'order-schema' + ); + const userResult = parsed.results.find( + (r: { contract: string }) => r.contract === 'user-schema' + ); + + expect(orderResult.summary.breaking).toBeGreaterThan(0); + expect(userResult.summary.breaking).toBe(0); + expect(userResult.summary.nonBreaking).toBe(0); + }); + + test('changeset includes all changed contracts', () => { + const { dir } = tempRepo; + + // Use only json-schema contracts since openapi requires oasdiff binary + setupRepoWithConfig(dir, [ + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + // Set up snapshots + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/user-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current specs with changes - only order-schema changed + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); // No change + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + + // Check that changeset file was created + const changesetFiles = listFiles(dir, '.contractual/changesets').filter(f => f.endsWith('.md')); + expect(changesetFiles.length).toBeGreaterThan(0); + + // Read the changeset file (markdown with YAML frontmatter) + const changesetPath = `.contractual/changesets/${changesetFiles[0]}`; + const changesetContent = readFile(dir, changesetPath); + + // Changeset should include order-schema (which changed) + expect(changesetContent).toContain('order-schema'); + // user-schema should not be included (no changes) + expect(changesetContent).not.toContain('user-schema'); + }); + + test('version bumps multiple contracts independently', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + // Current specs + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Set up existing versions + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.5', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a changeset with different bump types - using markdown format with YAML frontmatter + const changesetId = `changeset-${Date.now()}`; + writeFile( + dir, + `.contractual/changesets/${changesetId}.md`, + `--- +"order-schema": major +"user-schema": patch +--- + +Multiple contracts bumped independently +` + ); + + // Run version command + const result = run('version', dir); + expect(result.exitCode).toBe(0); + + // Check versions were bumped correctly + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + + expect(versions['order-schema'].version).toBe('3.0.0'); // major bump: 2.0.0 -> 3.0.0 + expect(versions['user-schema'].version).toBe('1.0.6'); // patch bump: 1.0.5 -> 1.0.6 + + // Verify snapshots were updated + expect(fileExists(dir, '.contractual/snapshots/order-schema.json')).toBe(true); + expect(fileExists(dir, '.contractual/snapshots/user-schema.json')).toBe(true); + + // Changeset should be consumed (removed) + expect(fileExists(dir, `.contractual/changesets/${changesetId}.md`)).toBe(false); + }); + + test('lint reports errors for invalid specs', () => { + const { dir } = tempRepo; + + // Use lint: false for OpenAPI to avoid petstore errors, focus on JSON Schema validation + setupRepoWithConfig(dir, [ + { name: 'valid-api', type: 'openapi', path: 'specs/valid.yaml', lint: false }, + { name: 'invalid-schema', type: 'json-schema', path: 'schemas/invalid.json' }, + { name: 'another-valid', type: 'json-schema', path: 'schemas/valid.json' }, + ]); + + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/valid.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/valid.json')); + // Write invalid JSON Schema + writeFile( + dir, + 'schemas/invalid.json', + JSON.stringify({ + type: 'objekt', // Invalid type value + properties: { + id: { type: 123 }, // type should be string + }, + }) + ); + + // JSON Schema linter finds errors in invalid-schema, so exit code is 1 + const result = run('lint --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + const parsed = JSON.parse(result.stdout); + // Should have 2 results (JSON Schema contracts only, OpenAPI has lint: false) + expect(parsed.results).toHaveLength(2); + // invalid-schema should have errors + const invalidResult = parsed.results.find((r: { contract: string }) => r.contract === 'invalid-schema'); + expect(invalidResult.errors.length).toBeGreaterThan(0); + }); + + test('breaking --contract filters to single contract in multi-contract repo', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { name: 'petstore-api', type: 'openapi', path: 'specs/petstore.yaml' }, + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + ]); + + // Set up snapshots + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore-api.yaml') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'petstore-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Both have breaking changes + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + // Filter to only check order-schema + const result = run('breaking --contract order-schema --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed = JSON.parse(result.stdout); + expect(parsed.results).toHaveLength(1); + expect(parsed.results[0].contract).toBe('order-schema'); + expect(parsed.results[0].summary.breaking).toBeGreaterThan(0); + }); + + test('status shows all contracts with their current versions', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { name: 'petstore-api', type: 'openapi', path: 'specs/petstore.yaml' }, + { name: 'order-schema', type: 'json-schema', path: 'schemas/order.json' }, + { name: 'user-schema', type: 'json-schema', path: 'schemas/user.json' }, + ]); + + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'petstore-api': { version: '1.2.0', released: '2026-01-01T00:00:00Z' }, + 'order-schema': { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.5', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Status command only outputs text format (no --format json option) + const result = run('status', dir); + expect(result.exitCode).toBe(0); + + // Check for text output containing contract info + expect(result.stdout).toMatch(/petstore-api/); + expect(result.stdout).toMatch(/openapi/); + expect(result.stdout).toMatch(/1\.2\.0/); + + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).toMatch(/json-schema/); + expect(result.stdout).toMatch(/2\.0\.0/); + + expect(result.stdout).toMatch(/user-schema/); + expect(result.stdout).toMatch(/1\.0\.5/); + }); +}); diff --git a/tests/e2e/09-differs.json-schema.test.ts b/tests/e2e/09-differs.json-schema.test.ts new file mode 100644 index 0000000..c0a2de1 --- /dev/null +++ b/tests/e2e/09-differs.json-schema.test.ts @@ -0,0 +1,756 @@ +import { describe, test, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +interface BreakingChange { + category: string; + severity: string; + path: string; + message: string; +} + +interface BreakingResult { + contract: string; + changes: BreakingChange[]; + summary: { + breaking: number; + nonBreaking: number; + patch: number; + }; +} + +interface BreakingOutput { + hasBreaking: boolean; + results: BreakingResult[]; +} + +describe('JSON Schema differ exhaustive tests', () => { + let tempRepo: { dir: string; cleanup: () => void }; + + beforeEach(() => { + tempRepo = createTempRepo(); + }); + + afterEach(() => { + tempRepo.cleanup(); + }); + + /** + * Helper to set up a JSON Schema diff test scenario + */ + function setupJsonSchemaDiff(baseFixture: string, currentFixture: string): void { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Set up base as snapshot (previous release) + copyFixture(baseFixture, path.join(dir, '.contractual/snapshots/order-schema.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Set up current spec + copyFixture(currentFixture, path.join(dir, 'schemas/order.json')); + } + + describe('breaking changes (exit 1, major)', () => { + test('field removed -> category: property-removed', () => { + setupJsonSchemaDiff('json-schema/order-base.json', 'json-schema/order-field-removed.json'); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const propertyRemovedChange = changes.find((c) => c.category === 'property-removed'); + expect(propertyRemovedChange).toBeDefined(); + expect(propertyRemovedChange!.severity).toBe('breaking'); + expect(propertyRemovedChange!.path).toContain('customer_email'); + }); + + test('required added -> category: required-added', () => { + setupJsonSchemaDiff('json-schema/order-base.json', 'json-schema/order-required-added.json'); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const requiredAddedChange = changes.find((c) => c.category === 'required-added'); + expect(requiredAddedChange).toBeDefined(); + expect(requiredAddedChange!.severity).toBe('breaking'); + // The path is /required, but the field name appears in the message + expect(requiredAddedChange!.path).toBe('/required'); + expect(requiredAddedChange!.message).toContain('customer_email'); + }); + + test('type changed -> category: type-changed', () => { + setupJsonSchemaDiff('json-schema/order-base.json', 'json-schema/order-type-changed.json'); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const typeChangedChange = changes.find((c) => c.category === 'type-changed'); + expect(typeChangedChange).toBeDefined(); + expect(typeChangedChange!.severity).toBe('breaking'); + expect(typeChangedChange!.path).toContain('amount'); + expect(typeChangedChange!.message).toMatch(/string.*number|type/i); + }); + + test('enum value removed -> category: enum-value-removed', () => { + setupJsonSchemaDiff( + 'json-schema/order-base.json', + 'json-schema/order-enum-value-removed.json' + ); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const enumRemovedChange = changes.find((c) => c.category === 'enum-value-removed'); + expect(enumRemovedChange).toBeDefined(); + expect(enumRemovedChange!.severity).toBe('breaking'); + expect(enumRemovedChange!.path).toContain('status'); + expect(enumRemovedChange!.message).toMatch(/delivered/i); + }); + + test('constraint tightened -> category: constraint-tightened', () => { + setupJsonSchemaDiff( + 'json-schema/order-base.json', + 'json-schema/order-constraint-tightened.json' + ); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const constraintChange = changes.find((c) => c.category === 'constraint-tightened'); + expect(constraintChange).toBeDefined(); + expect(constraintChange!.severity).toBe('breaking'); + // Should detect maxLength reduction in zip (10 -> 5) or notes (500 -> 200) + expect(constraintChange!.message).toMatch(/maxLength|constraint/i); + }); + + test('additionalProperties denied -> category: additional-properties-denied', () => { + setupJsonSchemaDiff( + 'json-schema/order-base.json', + 'json-schema/order-additional-props-denied.json' + ); + + const result = run('breaking --format json', tempRepo.dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const additionalPropsChange = changes.find( + (c) => c.category === 'additional-properties-denied' + ); + expect(additionalPropsChange).toBeDefined(); + expect(additionalPropsChange!.severity).toBe('breaking'); + expect(additionalPropsChange!.path).toContain('metadata'); + }); + }); + + describe('non-breaking changes (exit 0, minor)', () => { + test('optional field added -> category: property-added, severity: non-breaking', () => { + setupJsonSchemaDiff( + 'json-schema/order-base.json', + 'json-schema/order-optional-field-added.json' + ); + + const result = run('breaking --format json', tempRepo.dir); + expect(result.exitCode).toBe(0); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(false); + + const changes = parsed.results[0].changes; + const propertyAddedChange = changes.find((c) => c.category === 'property-added'); + expect(propertyAddedChange).toBeDefined(); + expect(propertyAddedChange!.severity).toBe('non-breaking'); + expect(propertyAddedChange!.path).toContain('tracking_number'); + + // Summary should show non-breaking changes + expect(parsed.results[0].summary.nonBreaking).toBeGreaterThan(0); + expect(parsed.results[0].summary.breaking).toBe(0); + }); + + test('enum value added -> category: enum-value-added', () => { + setupJsonSchemaDiff('json-schema/order-base.json', 'json-schema/order-enum-value-added.json'); + + const result = run('breaking --format json', tempRepo.dir); + expect(result.exitCode).toBe(0); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(false); + + const changes = parsed.results[0].changes; + const enumAddedChange = changes.find((c) => c.category === 'enum-value-added'); + expect(enumAddedChange).toBeDefined(); + expect(enumAddedChange!.severity).toBe('non-breaking'); + expect(enumAddedChange!.path).toContain('status'); + expect(enumAddedChange!.message).toMatch(/cancelled/i); + }); + }); + + describe('patch changes (exit 0, patch)', () => { + test('description changed -> category: description-changed', () => { + setupJsonSchemaDiff( + 'json-schema/order-base.json', + 'json-schema/order-description-changed.json' + ); + + const result = run('breaking --format json', tempRepo.dir); + expect(result.exitCode).toBe(0); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(false); + + const changes = parsed.results[0].changes; + const descriptionChange = changes.find((c) => c.category === 'description-changed'); + expect(descriptionChange).toBeDefined(); + expect(descriptionChange!.severity).toBe('patch'); + + // Summary should show patch changes + expect(parsed.results[0].summary.patch).toBeGreaterThan(0); + expect(parsed.results[0].summary.breaking).toBe(0); + }); + }); + + describe('no changes', () => { + test('identical schema -> no changes detected', () => { + setupJsonSchemaDiff('json-schema/order-base.json', 'json-schema/order-identical.json'); + + const result = run('breaking --format json', tempRepo.dir); + expect(result.exitCode).toBe(0); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(false); + expect(parsed.results[0].changes).toHaveLength(0); + expect(parsed.results[0].summary.breaking).toBe(0); + expect(parsed.results[0].summary.nonBreaking).toBe(0); + expect(parsed.results[0].summary.patch).toBe(0); + }); + }); + + describe('multiple changes in single diff', () => { + test('detects multiple breaking changes in one schema update', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Use base as snapshot + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a schema with multiple breaking changes + const multiBreakingSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://example.com/order.schema.json', + title: 'Order', + description: 'An order in the system', + type: 'object', + properties: { + id: { + type: 'number', // Type changed from string + description: 'Unique order identifier', + }, + status: { + type: 'string', + enum: ['pending', 'processing'], // Removed 'shipped' and 'delivered' + description: 'Current order status', + }, + // 'amount' property removed + currency: { + type: 'string', + format: 'iso-4217', + description: 'Currency code (ISO 4217)', + }, + items: { + type: 'array', + description: 'Line items in the order', + items: { + type: 'object', + properties: { + sku: { type: 'string' }, + quantity: { type: 'integer', minimum: 1 }, + price: { type: 'string' }, + }, + required: ['sku', 'quantity', 'price'], + }, + }, + created_at: { + type: 'string', + format: 'date-time', + description: 'Order creation timestamp', + }, + }, + required: ['id', 'status', 'currency', 'items', 'created_at', 'customer_email'], // Added required field + additionalProperties: false, + }; + + writeFile(dir, 'schemas/order.json', JSON.stringify(multiBreakingSchema, null, 2)); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const breakingChanges = changes.filter((c) => c.severity === 'breaking'); + + // Should detect multiple breaking changes + expect(breakingChanges.length).toBeGreaterThanOrEqual(2); + + // Check for specific categories + const categories = breakingChanges.map((c) => c.category); + expect(categories).toContain('type-changed'); + expect( + categories.includes('enum-value-removed') || categories.includes('property-removed') + ).toBe(true); + }); + + test('detects mix of breaking and non-breaking changes', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create a schema with both breaking and non-breaking changes + const mixedChangesSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://example.com/order.schema.json', + title: 'Order', + description: 'An order in the e-commerce system', // Description changed (patch) + type: 'object', + properties: { + id: { + type: 'number', // Type changed (breaking) + description: 'Unique order identifier', + }, + status: { + type: 'string', + enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'], // Added enum value (non-breaking) + description: 'Current order status', + }, + amount: { + type: 'string', + description: 'Order total amount as string', + }, + currency: { + type: 'string', + format: 'iso-4217', + description: 'Currency code (ISO 4217)', + }, + items: { + type: 'array', + description: 'Line items in the order', + items: { + type: 'object', + properties: { + sku: { type: 'string' }, + quantity: { type: 'integer', minimum: 1 }, + price: { type: 'string' }, + }, + required: ['sku', 'quantity', 'price'], + }, + }, + shipping_address: { + type: 'object', + description: 'Shipping address', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + zip: { type: 'string', maxLength: 10 }, + country: { type: 'string' }, + }, + required: ['street', 'city', 'zip', 'country'], + }, + customer_email: { + type: 'string', + format: 'email', + description: 'Customer email address', + }, + notes: { + type: 'string', + description: 'Optional order notes', + maxLength: 500, + }, + created_at: { + type: 'string', + format: 'date-time', + description: 'Order creation timestamp', + }, + metadata: { + type: 'object', + description: 'Additional metadata', + additionalProperties: true, + }, + tracking_number: { + // New optional property (non-breaking) + type: 'string', + description: 'Shipment tracking number', + }, + }, + required: ['id', 'status', 'amount', 'currency', 'items', 'created_at'], + additionalProperties: false, + }; + + writeFile(dir, 'schemas/order.json', JSON.stringify(mixedChangesSchema, null, 2)); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); // Exit 1 because there are breaking changes + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const { summary } = parsed.results[0]; + expect(summary.breaking).toBeGreaterThan(0); + expect(summary.nonBreaking).toBeGreaterThan(0); + }); + }); + + describe('nested property changes', () => { + test('detects changes in nested object properties', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create schema with nested property changes (shipping_address.zip maxLength changed) + const nestedChangeSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://example.com/order.schema.json', + title: 'Order', + description: 'An order in the system', + type: 'object', + properties: { + id: { type: 'string', description: 'Unique order identifier' }, + status: { + type: 'string', + enum: ['pending', 'processing', 'shipped', 'delivered'], + description: 'Current order status', + }, + amount: { type: 'string', description: 'Order total amount as string' }, + currency: { + type: 'string', + format: 'iso-4217', + description: 'Currency code (ISO 4217)', + }, + items: { + type: 'array', + description: 'Line items in the order', + items: { + type: 'object', + properties: { + sku: { type: 'string' }, + quantity: { type: 'integer', minimum: 1 }, + price: { type: 'string' }, + }, + required: ['sku', 'quantity', 'price'], + }, + }, + shipping_address: { + type: 'object', + description: 'Shipping address', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + zip: { type: 'string', maxLength: 5 }, // Changed from 10 to 5 (breaking) + country: { type: 'string' }, + }, + required: ['street', 'city', 'zip', 'country'], + }, + customer_email: { + type: 'string', + format: 'email', + description: 'Customer email address', + }, + notes: { type: 'string', description: 'Optional order notes', maxLength: 500 }, + created_at: { + type: 'string', + format: 'date-time', + description: 'Order creation timestamp', + }, + metadata: { + type: 'object', + description: 'Additional metadata', + additionalProperties: true, + }, + }, + required: ['id', 'status', 'amount', 'currency', 'items', 'created_at'], + additionalProperties: false, + }; + + writeFile(dir, 'schemas/order.json', JSON.stringify(nestedChangeSchema, null, 2)); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const constraintChange = changes.find((c) => c.category === 'constraint-tightened'); + expect(constraintChange).toBeDefined(); + expect(constraintChange!.path).toContain('shipping_address'); + expect(constraintChange!.path).toContain('zip'); + }); + + test('detects changes in array item schema', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create schema with array item changes (items[].sku type changed) + const arrayItemChangeSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://example.com/order.schema.json', + title: 'Order', + description: 'An order in the system', + type: 'object', + properties: { + id: { type: 'string', description: 'Unique order identifier' }, + status: { + type: 'string', + enum: ['pending', 'processing', 'shipped', 'delivered'], + description: 'Current order status', + }, + amount: { type: 'string', description: 'Order total amount as string' }, + currency: { + type: 'string', + format: 'iso-4217', + description: 'Currency code (ISO 4217)', + }, + items: { + type: 'array', + description: 'Line items in the order', + items: { + type: 'object', + properties: { + sku: { type: 'number' }, // Changed from string to number (breaking) + quantity: { type: 'integer', minimum: 1 }, + price: { type: 'string' }, + }, + required: ['sku', 'quantity', 'price'], + }, + }, + shipping_address: { + type: 'object', + description: 'Shipping address', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + zip: { type: 'string', maxLength: 10 }, + country: { type: 'string' }, + }, + required: ['street', 'city', 'zip', 'country'], + }, + customer_email: { + type: 'string', + format: 'email', + description: 'Customer email address', + }, + notes: { type: 'string', description: 'Optional order notes', maxLength: 500 }, + created_at: { + type: 'string', + format: 'date-time', + description: 'Order creation timestamp', + }, + metadata: { + type: 'object', + description: 'Additional metadata', + additionalProperties: true, + }, + }, + required: ['id', 'status', 'amount', 'currency', 'items', 'created_at'], + additionalProperties: false, + }; + + writeFile(dir, 'schemas/order.json', JSON.stringify(arrayItemChangeSchema, null, 2)); + + const result = run('breaking --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + + const parsed: BreakingOutput = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(true); + + const changes = parsed.results[0].changes; + const typeChange = changes.find((c) => c.category === 'type-changed'); + expect(typeChange).toBeDefined(); + expect(typeChange!.path).toContain('items'); + expect(typeChange!.path).toContain('sku'); + }); + }); + + describe('edge cases', () => { + test('handles first version (no snapshot)', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // No snapshot exists - first version + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('breaking --format json', dir); + expect(result.exitCode).toBe(0); + + const parsed = JSON.parse(result.stdout); + expect(parsed.hasBreaking).toBe(false); + // First version with no snapshot shows the contract with no changes + if (parsed.results.length > 0) { + expect(parsed.results[0].changes).toHaveLength(0); + } + }); + + test('handles contract with breaking detection disabled', () => { + const { dir } = tempRepo; + + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + breaking: false, + }, + ]); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Even with breaking changes, should pass because breaking detection is disabled + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + // When breaking detection is disabled, shows "No changes detected" + expect(result.stdout).toMatch(/no changes detected/i); + }); + }); +}); diff --git a/tests/e2e/10-openapi-differ.test.ts b/tests/e2e/10-openapi-differ.test.ts new file mode 100644 index 0000000..984ec81 --- /dev/null +++ b/tests/e2e/10-openapi-differ.test.ts @@ -0,0 +1,247 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('OpenAPI differ (native)', () => { + test('detects breaking change when endpoint is removed', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot (simulating previous release) + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec has endpoint removed (breaking change) + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('detects non-breaking change when endpoint is added', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec has new endpoint added (non-breaking change) + copyFixture( + 'openapi/petstore-nonbreaking-endpoint-added.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + // Non-breaking changes should exit 0 + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/non-breaking|minor|no breaking/i); + } finally { + cleanup(); + } + }); + + test('reports no changes when specs are identical', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec is identical to snapshot + copyFixture('openapi/petstore-identical.yaml', path.join(dir, 'specs/petstore.yaml')); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no.*change|no breaking|identical/i); + } finally { + cleanup(); + } + }); + + test('detects breaking change when response type is changed', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec has type change (breaking) + copyFixture( + 'openapi/petstore-breaking-type-changed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('detects non-breaking change when only description is updated', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec has only description changed (non-breaking) + copyFixture( + 'openapi/petstore-nonbreaking-description.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('returns JSON output with --format json flag', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + // Set up base spec as snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec has breaking change + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking --format json', dir, { expectFail: true }); + const parsed = JSON.parse(result.stdout); + + expect(parsed).toHaveProperty('results'); + expect(parsed).toHaveProperty('hasBreaking'); + expect(parsed.hasBreaking).toBe(true); + expect(Array.isArray(parsed.results)).toBe(true); + expect(parsed.results.length).toBeGreaterThan(0); + expect(parsed.results[0]).toHaveProperty('contract', 'petstore'); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/11-custom-commands.test.ts b/tests/e2e/11-custom-commands.test.ts new file mode 100644 index 0000000..8edeb72 --- /dev/null +++ b/tests/e2e/11-custom-commands.test.ts @@ -0,0 +1,330 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + fileExists, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('custom command overrides', () => { + describe('custom lint command', () => { + test('uses custom lint command with {spec} placeholder', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create a custom lint script that creates a marker file + writeFile( + dir, + 'custom-lint.sh', + `#!/bin/bash +echo "Custom lint executed for: $1" +touch "${dir}/lint-marker.txt" +exit 0 +` + ); + + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: `bash ${dir}/custom-lint.sh {spec}`, + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + // Custom command was executed - marker file should exist + expect(fileExists(dir, 'lint-marker.txt')).toBe(true); + } finally { + cleanup(); + } + }); + + test('custom lint command without {spec} uses default linter', () => { + const { dir, cleanup } = createTempRepo(); + try { + // A lint override that doesn't contain {spec} falls through to default linter + // The petstore fixture has lint errors, so the default linter will exit 1 + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: 'exit 1', // No {spec} - falls through to default linter + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Default OpenAPI linter runs and finds errors in the petstore fixture + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + // Should show lint errors from the default linter + expect(result.stdout).toMatch(/error/i); + } finally { + cleanup(); + } + }); + + test('lint: false disables linting for contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: false, + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + // Spinner output goes to TTY, not captured in stdout + // JSON format shows empty results when linting is disabled + const jsonResult = run('lint --format json', dir); + const parsed = JSON.parse(jsonResult.stdout); + expect(parsed.results).toHaveLength(0); + expect(parsed.errors).toBe(0); + } finally { + cleanup(); + } + }); + + test('lint: false with invalid spec still passes (linting skipped)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'bad-spec', + type: 'openapi', + path: 'specs/bad.yaml', + lint: false, + }, + ]); + // Write intentionally invalid OpenAPI + writeFile( + dir, + 'specs/bad.yaml', + `openapi: 3.0.0 +info: + title: Invalid + # Missing version field - normally invalid +paths: [] # paths should be object, not array +` + ); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + // Verify via JSON that the contract was skipped (not in results) + const jsonResult = run('lint --format json', dir); + const parsed = JSON.parse(jsonResult.stdout); + expect(parsed.results).toHaveLength(0); + } finally { + cleanup(); + } + }); + }); + + describe('custom breaking command', () => { + test('uses custom breaking command with {old} and {new} placeholders', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create a custom breaking detection script + writeFile( + dir, + 'custom-breaking.sh', + `#!/bin/bash +echo "Comparing old: $1 with new: $2" +touch "${dir}/breaking-marker.txt" +exit 0 +` + ); + + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + breaking: `bash ${dir}/custom-breaking.sh {old} {new}`, + }, + ]); + + // Set up snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Current spec with changes + copyFixture( + 'openapi/petstore-nonbreaking-endpoint-added.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + // Custom command was executed - marker file should exist + expect(fileExists(dir, 'breaking-marker.txt')).toBe(true); + } finally { + cleanup(); + } + }); + + test('custom breaking command without placeholders uses default differ', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Custom command without {old} or {new} falls through to default differ + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + breaking: 'echo "Breaking change detected" && exit 1', + }, + ]); + + // Set up snapshot + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + // Same spec as snapshot - no changes, default differ will pass + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Default differ runs - with identical specs, should pass (no breaking changes) + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('breaking: false disables breaking detection for contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + breaking: false, + }, + ]); + + // Set up snapshot with breaking change in current + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + // Even with a breaking change, it should pass because breaking detection is disabled + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + // When breaking detection is disabled, shows "No changes detected" + expect(result.stdout).toMatch(/no changes detected/i); + } finally { + cleanup(); + } + }); + }); + + describe('mixed custom and default commands', () => { + test('can mix custom lint with default breaking', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Custom lint command with {spec} - will be executed + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: 'echo "Custom lint for {spec}" && exit 0', + // No custom breaking - uses default + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Custom lint command with {spec} runs and exits 0 + const lintResult = run('lint', dir); + expect(lintResult.exitCode).toBe(0); + + // Default breaking (no snapshot) - exits 0 + const breakingResult = run('breaking', dir); + expect(breakingResult.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('can have different settings per contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'api-v1', + type: 'openapi', + path: 'specs/api-v1.yaml', + lint: false, // Disable lint for v1 + breaking: false, // Disable breaking for v1 + }, + { + name: 'api-v2', + type: 'openapi', + path: 'specs/api-v2.yaml', + // Use defaults for v2 - will use default OpenAPI linter + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api-v1.yaml')); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api-v2.yaml')); + + // The petstore fixture has lint errors, so api-v2 will cause exit 1 + const result = run('lint --format json', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + const parsed = JSON.parse(result.stdout); + + // api-v1 is skipped (lint: false), api-v2 is linted with default linter + expect(parsed.results).toHaveLength(1); + expect(parsed.results[0].contract).toBe('api-v2'); + // api-v2 has lint errors from the petstore fixture + expect(parsed.errors).toBeGreaterThan(0); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/11-openapi-31-differ.test.ts b/tests/e2e/11-openapi-31-differ.test.ts new file mode 100644 index 0000000..7b9c241 --- /dev/null +++ b/tests/e2e/11-openapi-31-differ.test.ts @@ -0,0 +1,204 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('OpenAPI 3.1 differ', () => { + test('detects breaking change when endpoint is removed (3.1 spec)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + copyFixture( + 'openapi/petstore-31-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('detects breaking change when type is narrowed (3.1 type arrays)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // tag field narrowed from ["string", "null"] to "string" + copyFixture( + 'openapi/petstore-31-breaking-type-narrowed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('detects non-breaking change when type is widened (3.1 type arrays)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // id field widened from integer to [integer, string] + copyFixture( + 'openapi/petstore-31-nonbreaking-type-widened.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('reports no changes when 3.1 specs are identical', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Same spec as snapshot + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no.*change|no breaking|identical/i); + } finally { + cleanup(); + } + }); + + test('returns JSON output for 3.1 spec with --format json', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + copyFixture( + 'openapi/petstore-31-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking --format json', dir, { expectFail: true }); + const parsed = JSON.parse(result.stdout); + + expect(parsed).toHaveProperty('results'); + expect(parsed).toHaveProperty('hasBreaking'); + expect(parsed.hasBreaking).toBe(true); + expect(Array.isArray(parsed.results)).toBe(true); + expect(parsed.results.length).toBeGreaterThan(0); + expect(parsed.results[0]).toHaveProperty('contract', 'petstore'); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/12-edge-cases.test.ts b/tests/e2e/12-edge-cases.test.ts new file mode 100644 index 0000000..58c1a27 --- /dev/null +++ b/tests/e2e/12-edge-cases.test.ts @@ -0,0 +1,510 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, + fileExists, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('error handling and edge cases', () => { + describe('missing contractual.yaml', () => { + test('exits with error suggesting init when no config exists', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Run lint without any config + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/contractual\.yaml|not found|init/i); + } finally { + cleanup(); + } + }); + + test('breaking command suggests init when no config exists', () => { + const { dir, cleanup } = createTempRepo(); + try { + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/contractual\.yaml|not found|init/i); + } finally { + cleanup(); + } + }); + + test('status command suggests init when no config exists', () => { + const { dir, cleanup } = createTempRepo(); + try { + const result = run('status', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/contractual\.yaml|not found|init/i); + } finally { + cleanup(); + } + }); + }); + + describe('invalid YAML configuration', () => { + test('exits with parse error for malformed YAML', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write invalid YAML syntax + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + - name: broken + type: [this is invalid yaml + path: nowhere +` + ); + + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/parse|yaml|syntax|invalid/i); + } finally { + cleanup(); + } + }); + + test('exits with error for invalid config structure', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write valid YAML but invalid config structure + writeFile( + dir, + 'contractual.yaml', + `contracts: "this should be an array" +` + ); + + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/invalid|error|array/i); + } finally { + cleanup(); + } + }); + + test('handles empty config file gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile(dir, 'contractual.yaml', ''); + + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/empty|invalid|error/i); + } finally { + cleanup(); + } + }); + }); + + describe('spec file not found', () => { + test('lint reports error when spec file does not exist', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'missing-api', + type: 'openapi', + path: 'specs/nonexistent.yaml', + }, + ]); + + // Config loader warns about missing files but continues + // Linter will fail when trying to read the missing file + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/warning|not found|no such file|ENOENT/i); + } finally { + cleanup(); + } + }); + + test('breaking command handles missing spec file gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'missing-api', + type: 'openapi', + path: 'specs/nonexistent.yaml', + }, + ]); + // Set up snapshot but no current spec + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/missing-api.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'missing-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Config loader warns about missing file but continues + // Breaking command may fail during diff (if oasdiff available) or report no differ + const result = run('breaking', dir, { expectFail: true }); + // Should mention the missing file or failed check in output + expect(result.stdout + result.stderr).toMatch(/warning|not found|failed|error|no contracts/i); + } finally { + cleanup(); + } + }); + + test('lints valid contracts and reports error for missing spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'valid-api', + type: 'openapi', + path: 'specs/valid.yaml', + }, + { + name: 'missing-api', + type: 'openapi', + path: 'specs/nonexistent.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/valid.yaml')); + + // Config loader emits a warning about missing files but continues + // Lint will process valid contract and fail on missing one + const result = run('lint', dir, { expectFail: true }); + // Should mention the missing file + expect(result.stdout + result.stderr).toMatch(/nonexistent|not found|ENOENT/i); + } finally { + cleanup(); + } + }); + }); + + describe('corrupt versions.json', () => { + test('status handles corrupt/invalid JSON in versions.json gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Write corrupt JSON to versions.json + writeFile(dir, '.contractual/versions.json', '{ this is not valid json }}}'); + + // The readVersions function in status.command.ts returns {} on parse error + // So the status command should succeed, treating all contracts as unreleased + const result = run('status', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/unreleased|0\.0\.0/i); + } finally { + cleanup(); + } + }); + + test('status handles versions.json with wrong structure gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Write valid JSON but wrong structure (array instead of object) + // This will be treated as invalid, and status will show contracts as unreleased + writeFile(dir, '.contractual/versions.json', JSON.stringify(['not', 'an', 'object'])); + + const result = run('status', dir); + // Status command should succeed - it may treat versions as empty + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('breaking handles missing snapshot gracefully', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // No snapshot exists for this contract - first version + const result = run('breaking', dir); + // Should handle gracefully - exits with 0 + expect(result.exitCode).toBe(0); + // When no snapshot exists, shows "No changes detected" + expect(result.stdout).toMatch(/no changes detected/i); + } finally { + cleanup(); + } + }); + }); + + describe('changeset references unknown contract', () => { + test('status shows changeset referencing unknown contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Create a changeset that references an unknown contract + // Format: YAML frontmatter with "contract-name": bump-type + writeFile( + dir, + '.contractual/changesets/orphan-change.md', + `--- +"unknown-contract": minor +--- + +This references a contract that does not exist +` + ); + + // Status command should succeed - it shows the changeset + // but the projected version won't be shown for an unknown contract + const result = run('status', dir); + expect(result.exitCode).toBe(0); + // Should list the changeset + expect(result.stdout).toMatch(/changeset|pending/i); + } finally { + cleanup(); + } + }); + + test('version command handles changeset with unknown contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Create a valid changeset for known contract + writeFile( + dir, + '.contractual/changesets/valid-change.md', + `--- +"petstore": patch +--- + +A valid change +` + ); + + // Create a changeset for unknown contract + writeFile( + dir, + '.contractual/changesets/orphan-change.md', + `--- +"ghost-api": major +--- + +This references a non-existent contract +` + ); + + const result = run('version', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('ghost-api'); + expect(fileExists(dir, '.contractual/changesets/valid-change.md')).toBe(true); + expect(fileExists(dir, '.contractual/changesets/orphan-change.md')).toBe(true); + } finally { + cleanup(); + } + }); + }); + + describe('empty contracts array', () => { + test('lint rejects empty contracts array per schema validation', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, []); + + // Empty contracts array is invalid per config schema (minItems: 1) + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/invalid|error|contract|minItems/i); + } finally { + cleanup(); + } + }); + + test('breaking rejects empty contracts array per schema validation', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, []); + + // Empty contracts array is invalid per config schema (minItems: 1) + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/invalid|error|contract|minItems/i); + } finally { + cleanup(); + } + }); + + test('status rejects empty contracts array per schema validation', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, []); + + // Empty contracts array is invalid per config schema (minItems: 1) + const result = run('status', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/invalid|error|contract|minItems/i); + } finally { + cleanup(); + } + }); + }); + + describe('special characters in paths', () => { + test('handles spec path with spaces', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'spaced-api', + type: 'openapi', + path: 'specs/my api spec.yaml', + lint: false, // Disable linting to avoid errors from fixture spec + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/my api spec.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('handles contract name with dashes and numbers', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Contract names must match pattern: ^[a-z][a-z0-9-]*$ + // So dashes and numbers are allowed, but not dots + setupRepoWithConfig(dir, [ + { + name: 'api-v2-beta', + type: 'openapi', + path: 'specs/api.yaml', + lint: false, // Disable linting to avoid errors from fixture spec + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + }); + + describe('permission and file system errors', () => { + test('lint reports permission error when spec file is unreadable', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Make file unreadable (only works on Unix-like systems) + try { + require('node:fs').chmodSync(path.join(dir, 'specs/petstore.yaml'), 0o000); + } catch { + // Skip this test on systems where chmod doesn't work + return; + } + + try { + // Linter will fail when trying to read the file + const result = run('lint', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout + result.stderr).toMatch(/permission|EACCES|denied|error/i); + } finally { + // Restore permissions for cleanup + require('node:fs').chmodSync(path.join(dir, 'specs/petstore.yaml'), 0o644); + } + } finally { + cleanup(); + } + }); + }); + + describe('concurrent operations', () => { + test('handles being run in parallel (no race conditions)', async () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: false, // Disable linting to avoid errors from fixture spec + breaking: false, // Disable breaking to avoid needing snapshot + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Run multiple commands in parallel + // Note: status command does not have --format flag + const results = await Promise.all([ + Promise.resolve(run('lint', dir)), + Promise.resolve(run('status', dir)), + Promise.resolve(run('breaking', dir)), + ]); + + // All should succeed + for (const result of results) { + expect(result.exitCode).toBe(0); + } + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/12-openapi-circular-refs.test.ts b/tests/e2e/12-openapi-circular-refs.test.ts new file mode 100644 index 0000000..fb94d65 --- /dev/null +++ b/tests/e2e/12-openapi-circular-refs.test.ts @@ -0,0 +1,91 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('OpenAPI differ with circular $ref schemas', () => { + test('handles identical specs with circular refs without stack overflow', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'circular-api', + type: 'openapi', + path: 'specs/api.yaml', + }, + ]); + + copyFixture( + 'openapi/circular-refs-base.yaml', + path.join(dir, '.contractual/snapshots/circular-api.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'circular-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Same spec β€” should produce no changes, not a stack overflow + copyFixture( + 'openapi/circular-refs-base.yaml', + path.join(dir, 'specs/api.yaml') + ); + + // 10s timeout β€” stack overflow would hang/crash without it + const result = run('breaking', dir, { timeout: 10_000 }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no.*change|no breaking|identical/i); + } finally { + cleanup(); + } + }); + + test('detects breaking change in schema with circular refs', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'circular-api', + type: 'openapi', + path: 'specs/api.yaml', + }, + ]); + + copyFixture( + 'openapi/circular-refs-base.yaml', + path.join(dir, '.contractual/snapshots/circular-api.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'circular-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Category.id changed from integer to string β€” breaking + copyFixture( + 'openapi/circular-refs-breaking.yaml', + path.join(dir, 'specs/api.yaml') + ); + + const result = run('breaking', dir, { expectFail: true, timeout: 10_000 }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/13-config-variations.test.ts b/tests/e2e/13-config-variations.test.ts new file mode 100644 index 0000000..1d14248 --- /dev/null +++ b/tests/e2e/13-config-variations.test.ts @@ -0,0 +1,545 @@ +import { describe, test, expect, beforeAll, vi } from 'vitest'; +import path from 'node:path'; +import { ConfigError, resolveContractPaths } from '../../packages/cli/src/config/loader.js'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readYAML, + fileExists, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('config variations', () => { + test('minimal config (just contracts array) works', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write minimal config with only required fields + // Use lint: false to avoid petstore fixture lint errors + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: false +` + ); + + // Create .contractual directory structure + writeFile(dir, '.contractual/versions.json', '{}'); + + // Copy the spec file + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + // Lint should work with minimal config (lint disabled) + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + // Verify config was parsed correctly + const config = readYAML(dir, 'contractual.yaml') as { contracts: Array<{ name: string; type: string; path: string }> }; + expect(config.contracts).toHaveLength(1); + expect(config.contracts[0].name).toBe('api'); + expect(config.contracts[0].type).toBe('openapi'); + } finally { + cleanup(); + } + }); + + test('config with changeset options (autoDetect, requireOnPR)', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write config with changeset options + // Use lint: false to avoid petstore fixture lint errors + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: false +changeset: + autoDetect: true + requireOnPR: false +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + // Config should be valid and lint should work + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + // Verify changeset options are preserved + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array; + changeset: { autoDetect: boolean; requireOnPR: boolean }; + }; + expect(config.changeset).toBeDefined(); + expect(config.changeset.autoDetect).toBe(true); + expect(config.changeset.requireOnPR).toBe(false); + } finally { + cleanup(); + } + }); + + test('config with AI options (disabled by default)', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write config with AI options - all features disabled + // Use lint: false to avoid petstore fixture lint errors + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: false +ai: + provider: anthropic + model: claude-3-sonnet + features: + explain: false + changelog: false + enhance: false +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + // Config should be valid + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + // Verify AI options are preserved + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array; + ai: { + provider: string; + model: string; + features: { explain: boolean; changelog: boolean; enhance: boolean }; + }; + }; + expect(config.ai).toBeDefined(); + expect(config.ai.provider).toBe('anthropic'); + expect(config.ai.model).toBe('claude-3-sonnet'); + expect(config.ai.features.explain).toBe(false); + expect(config.ai.features.changelog).toBe(false); + expect(config.ai.features.enhance).toBe(false); + } finally { + cleanup(); + } + }); + + test('glob patterns in contract path work', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write config with glob pattern and lint disabled + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: all-schemas + type: json-schema + path: schemas/*.json + lint: false +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + + // Create multiple schema files matching the glob + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Lint should work with glob pattern (lint disabled) + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + // Verify the file exists at the expected location + expect(fileExists(dir, 'schemas/order.json')).toBe(true); + } finally { + cleanup(); + } + }); + + test.each([ + ['reported deeply nested braces', '{'.repeat(4500) + 'x' + '}'.repeat(4500) + '*', '1024 characters'], + ['oversized flat glob', 'x'.repeat(1024) + '*', '1024 characters'], + ['nested braces', '{'.repeat(33) + 'x' + '}'.repeat(33) + '*', '32 levels'], + ['nested parentheses', '('.repeat(33) + 'x' + ')'.repeat(33) + '*', '32 levels'], + ['mixed nesting', '{('.repeat(17) + 'x' + ')}'.repeat(17) + '*', '32 levels'], + ['quoted closing braces', '{"}"'.repeat(33) + '*', '32 levels'], + ['character-class closing braces', '{[}]'.repeat(33) + '*', '32 levels'], + ['escaped closing braces', '{\\}'.repeat(33) + '*', '32 levels'], + ['mismatched closing braces', '(}'.repeat(33) + '*', '32 levels'], + ])('rejects %s as a controlled configuration error', (_name, pattern, limit) => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile(dir, 'contractual.yaml', JSON.stringify({ + contracts: [{ name: 'api', type: 'openapi', path: pattern }], + })); + + const result = run('status', dir, { expectFail: true, timeout: 5000 }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Glob for contract "api" exceeds'); + expect(result.stderr).toContain(limit); + expect(result.stderr).not.toContain('Maximum call stack'); + expect(result.stderr).not.toContain(pattern); + } finally { + cleanup(); + } + }); + + test.each([ + 'schemas/{order,other}.j*', + 'schemas/{order,{other,third}}.j*', + 'schemas/@(order|other).j*', + 'schemas/[o]rder.j*', + ])('preserves normal glob matching: %s', (pattern) => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + const contracts = resolveContractPaths({ + contracts: [{ name: 'order', type: 'json-schema', path: pattern }], + }, dir); + expect(contracts).toHaveLength(1); + expect(contracts[0].absolutePath).toBe(path.join(dir, 'schemas/order.json')); + } finally { + cleanup(); + } + }); + + test('bounds the full resolved pattern and accepts the exact length limit', () => { + const { dir, cleanup } = createTempRepo(); + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + const prefixLength = path.resolve(dir).length + path.sep.length; + const pattern = 'x'.repeat(1024 - prefixLength - 1) + '*'; + const config = { contracts: [{ name: 'api', type: 'openapi' as const, path: pattern }] }; + expect(() => resolveContractPaths(config, dir)).not.toThrow(); + config.contracts[0].path += '*'; + expect(() => resolveContractPaths(config, dir)).toThrow(ConfigError); + expect(() => resolveContractPaths(config, dir)).toThrow('1024 characters'); + } finally { + warning.mockRestore(); + cleanup(); + } + }); + + test.each([ + ['exact nesting limit', '{'.repeat(32) + 'x' + '}'.repeat(32) + '*'], + ['escaped braces', '\\{'.repeat(40) + 'x' + '\\}'.repeat(40) + '*'], + ['quoted braces', '"' + '{'.repeat(40) + 'x' + '}'.repeat(40) + '"*'], + ['character-class braces', '[' + '{'.repeat(40) + ']' + '*'], + ])('accepts %s without treating literals as nesting', (_name, pattern) => { + const { dir, cleanup } = createTempRepo(); + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + expect(() => resolveContractPaths({ + contracts: [{ name: 'api', type: 'openapi', path: pattern }], + }, dir)).not.toThrow(); + } finally { + warning.mockRestore(); + cleanup(); + } + }); + + test('leaves literal paths with braces unchanged', () => { + const { dir, cleanup } = createTempRepo(); + try { + const relativePath = 'schemas/' + '{'.repeat(40) + 'order' + '}'.repeat(40) + '.json'; + copyFixture('json-schema/order-base.json', path.join(dir, relativePath)); + const contracts = resolveContractPaths({ + contracts: [{ name: 'order', type: 'json-schema', path: relativePath }], + }, dir); + expect(contracts[0].absolutePath).toBe(path.join(dir, relativePath)); + } finally { + cleanup(); + } + }); + + test('contract with all optional fields specified', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write config with all optional contract fields + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: full-contract + type: openapi + path: specs/api.yaml + lint: spectral + breaking: oasdiff + generate: + - openapi-generator generate -i specs/api.yaml -g typescript-axios -o ./client +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + // Use status command to verify config is valid (lint would fail due to petstore fixture errors) + const result = run('status', dir); + expect(result.exitCode).toBe(0); + + // Verify all optional fields are preserved + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ + name: string; + type: string; + path: string; + lint: string; + breaking: string; + generate: string[]; + }>; + }; + expect(config.contracts[0].lint).toBe('spectral'); + expect(config.contracts[0].breaking).toBe('oasdiff'); + expect(config.contracts[0].generate).toEqual([ + 'openapi-generator generate -i specs/api.yaml -g typescript-axios -o ./client', + ]); + } finally { + cleanup(); + } + }); + + test('config with multiple contract types and mixed options', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write complex config with multiple contracts and various options + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: spectral + - name: events + type: json-schema + path: schemas/events.json + lint: false + breaking: false +changeset: + autoDetect: true + requireOnPR: true +ai: + provider: anthropic + features: + explain: true + changelog: false +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/events.json')); + + // Use status command to verify config is valid (lint would fail due to petstore fixture errors) + const result = run('status', dir); + expect(result.exitCode).toBe(0); + + // Verify config structure + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ name: string; lint?: string | boolean; breaking?: string | boolean }>; + changeset: { autoDetect: boolean; requireOnPR: boolean }; + ai: { provider: string; features: { explain: boolean; changelog: boolean } }; + }; + expect(config.contracts).toHaveLength(2); + expect(config.contracts[0].lint).toBe('spectral'); + expect(config.contracts[1].lint).toBe(false); + expect(config.contracts[1].breaking).toBe(false); + expect(config.changeset.autoDetect).toBe(true); + expect(config.ai.features.explain).toBe(true); + } finally { + cleanup(); + } + }); + + test('config with nested directory structure for specs', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Write config pointing to deeply nested spec, with lint disabled + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: nested-api + type: openapi + path: src/contracts/api/v1/openapi.yaml + lint: false +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, 'src/contracts/api/v1/openapi.yaml') + ); + + // Lint should work with nested paths (lint disabled) + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + // Verify nested file exists + expect(fileExists(dir, 'src/contracts/api/v1/openapi.yaml')).toBe(true); + } finally { + cleanup(); + } + }); + + test('config with only changeset options (no ai)', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: false +changeset: + autoDetect: false + requireOnPR: true +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array; + changeset: { autoDetect: boolean; requireOnPR: boolean }; + ai?: unknown; + }; + expect(config.changeset.autoDetect).toBe(false); + expect(config.changeset.requireOnPR).toBe(true); + expect(config.ai).toBeUndefined(); + } finally { + cleanup(); + } + }); + + test('config with only ai options (no changeset)', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: api + type: openapi + path: specs/api.yaml + lint: false +ai: + provider: anthropic + features: + explain: true +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/api.yaml')); + + const result = run('lint', dir); + expect(result.exitCode).toBe(0); + + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array; + ai: { provider: string; features: { explain: boolean } }; + changeset?: unknown; + }; + expect(config.ai.provider).toBe('anthropic'); + expect(config.ai.features.explain).toBe(true); + expect(config.changeset).toBeUndefined(); + } finally { + cleanup(); + } + }); + + test('asyncapi contract type is accepted', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: events + type: asyncapi + path: specs/events.yaml +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + // Use a basic YAML file as placeholder (asyncapi would have similar structure) + writeFile( + dir, + 'specs/events.yaml', + `asyncapi: 2.6.0 +info: + title: Events API + version: 1.0.0 +channels: {} +` + ); + + // Config validation should pass (even if lint may not have asyncapi support yet) + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ name: string; type: string }>; + }; + expect(config.contracts[0].type).toBe('asyncapi'); + } finally { + cleanup(); + } + }); + + test('odcs contract type is accepted', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: data-contract + type: odcs + path: contracts/data.yaml +` + ); + + writeFile(dir, '.contractual/versions.json', '{}'); + // Use a basic YAML file as placeholder for ODCS + writeFile( + dir, + 'contracts/data.yaml', + `dataContractSpecification: 1.0.0 +info: + title: Data Contract + version: 1.0.0 +` + ); + + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ name: string; type: string }>; + }; + expect(config.contracts[0].type).toBe('odcs'); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/13-openapi-params-and-metadata.test.ts b/tests/e2e/13-openapi-params-and-metadata.test.ts new file mode 100644 index 0000000..b69e486 --- /dev/null +++ b/tests/e2e/13-openapi-params-and-metadata.test.ts @@ -0,0 +1,120 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +function setupPetstore31(dir: string) { + setupRepoWithConfig(dir, [ + { name: 'petstore', type: 'openapi', path: 'specs/petstore.yaml' }, + ]); + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); +} + +describe('OpenAPI parameter classification', () => { + test('adding optional parameter is non-breaking', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-nonbreaking-optional-param.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('adding required parameter is breaking', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-breaking-required-param.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('breaking', dir, { expectFail: true }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); +}); + +describe('OpenAPI metadata change detection', () => { + test('description/summary changes are detected as patch (not breaking)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-description-changed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + // Description changes should not be breaking + const result = run('breaking', dir); + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); + + test('description changes create a changeset', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-description-changed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('changeset', dir); + expect(result.exitCode).toBe(0); + // Should create a changeset (not "No changes detected") + expect(result.stdout).toMatch(/created changeset/i); + } finally { + cleanup(); + } + }); + + test('description changes detected in diff command', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-description-changed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff', dir); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/change|patch|description/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/14-openapi-message-formatting.test.ts b/tests/e2e/14-openapi-message-formatting.test.ts new file mode 100644 index 0000000..41898e4 --- /dev/null +++ b/tests/e2e/14-openapi-message-formatting.test.ts @@ -0,0 +1,181 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +function setupPetstore31(dir: string) { + setupRepoWithConfig(dir, [ + { name: 'petstore', type: 'openapi', path: 'specs/petstore.yaml' }, + ]); + copyFixture( + 'openapi/petstore-31-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); +} + +describe('OpenAPI change message formatting', () => { + test('path-added shows a readable message (not "Unknown change")', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'petstore', type: 'openapi', path: 'specs/petstore.yaml' }, + ]); + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + copyFixture( + 'openapi/petstore-nonbreaking-endpoint-added.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + const changes = parsed.contracts?.petstore?.changes ?? []; + + // Find the path-added change + const pathAdded = changes.find((c: { category: string }) => c.category === 'path-added'); + expect(pathAdded).toBeDefined(); + expect(pathAdded.message).not.toMatch(/unknown change/i); + expect(pathAdded.message).toMatch(/new path added/i); + } finally { + cleanup(); + } + }); + + test('path paths in messages are decoded from JSON Pointer', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'petstore', type: 'openapi', path: 'specs/petstore.yaml' }, + ]); + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff --format json', dir, { expectFail: false }); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + const changes = parsed.contracts?.petstore?.changes ?? []; + + // All messages should be decoded (no ~1 or ~0 escapes) + for (const change of changes) { + expect(change.message).not.toMatch(/~1/); + expect(change.message).not.toMatch(/~0/); + } + } finally { + cleanup(); + } + }); + + test('operation-added shows readable message', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-nonbreaking-optional-param.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + const changes = parsed.contracts?.petstore?.changes ?? []; + + // parameter-added case + const paramAdded = changes.find( + (c: { category: string }) => c.category === 'parameter-added' + ); + expect(paramAdded).toBeDefined(); + expect(paramAdded.message).not.toMatch(/unknown change/i); + expect(paramAdded.message).toMatch(/optional parameter added/i); + } finally { + cleanup(); + } + }); + + test('required parameter added shows specific message', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-breaking-required-param.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + const changes = parsed.contracts?.petstore?.changes ?? []; + + const reqParam = changes.find( + (c: { category: string }) => c.category === 'parameter-required-added' + ); + expect(reqParam).toBeDefined(); + expect(reqParam.message).not.toMatch(/unknown change/i); + expect(reqParam.message).toMatch(/required parameter added/i); + } finally { + cleanup(); + } + }); + + test('no "Unknown change" messages for any OpenAPI structural change', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupPetstore31(dir); + copyFixture( + 'openapi/petstore-31-description-changed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + const result = run('diff --format json', dir); + expect(result.exitCode).toBe(0); + const parsed = JSON.parse(result.stdout); + const changes = parsed.contracts?.petstore?.changes ?? []; + + expect(changes.length).toBeGreaterThan(0); + for (const change of changes) { + expect(change.message).not.toMatch(/unknown change/i); + } + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/14-strands-conformance.test.ts b/tests/e2e/14-strands-conformance.test.ts new file mode 100644 index 0000000..9da366a --- /dev/null +++ b/tests/e2e/14-strands-conformance.test.ts @@ -0,0 +1,607 @@ +/** + * Strands API Conformance Tests + * + * Tests the compareSchemas function against expected Strands API behavior. + * Based on the Strands API at https://strands.octue.com/api/compare-schemas + */ + +import { describe, test, expect } from 'vitest'; +import { + compareSchemas, + checkCompatibility, +} from '../../packages/differs.json-schema/src/compare.js'; +import type { + CompareResult, + CompareOptions, + StrandsTrace, + StrandsCompatibility, + StrandsVersion, + SemanticVersion, + ResolvedSchema, +} from '../../packages/differs.json-schema/src/types.js'; + +describe('Strands API Conformance', () => { + describe('Basic comparison results', () => { + test('identical schemas return equal version', () => { + const schema: ResolvedSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const result: CompareResult = compareSchemas(schema, schema); + expect(result.version).toBe('equal'); + expect(result.traces).toHaveLength(0); + }); + + test('no changes returns equal with currentVersion', () => { + const schema: ResolvedSchema = { type: 'string' }; + const options: CompareOptions = { currentVersion: '1.0.0' }; + const result: CompareResult = compareSchemas(schema, schema, options); + + expect(result.version).toBe('equal'); + const expectedVersion: SemanticVersion = { + major: 1, + minor: 0, + patch: 0, + version: '1.0.0', + }; + expect(result.newVersion).toEqual(expectedVersion); + }); + }); + + describe('Format changes are patch (not breaking)', () => { + test('format-added is patch', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'string', format: 'email' }; + + const result: CompareResult = compareSchemas(source, target, { currentVersion: '1.0.0' }); + expect(result.version).toBe('patch'); + expect(result.newVersion?.version).toBe('1.0.1'); + }); + + test('format-removed is patch', () => { + const source: ResolvedSchema = { type: 'string', format: 'email' }; + const target: ResolvedSchema = { type: 'string' }; + + const result: CompareResult = compareSchemas(source, target, { currentVersion: '1.0.0' }); + expect(result.version).toBe('patch'); + }); + + test('format-changed is patch', () => { + const source: ResolvedSchema = { type: 'string', format: 'email' }; + const target: ResolvedSchema = { type: 'string', format: 'uri' }; + + const result: CompareResult = compareSchemas(source, target, { currentVersion: '1.0.0' }); + expect(result.version).toBe('patch'); + }); + }); + + describe('Type changes classification', () => { + test('type change is breaking (major)', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + + const result: CompareResult = compareSchemas(source, target, { currentVersion: '1.0.0' }); + expect(result.version).toBe('major'); + expect(result.newVersion?.version).toBe('2.0.0'); + }); + + test('type narrowed is breaking', () => { + const source: ResolvedSchema = { type: ['string', 'number'] }; + const target: ResolvedSchema = { type: 'string' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('type widened is non-breaking (minor)', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: ['string', 'number'] }; + + const result: CompareResult = compareSchemas(source, target, { currentVersion: '1.0.0' }); + expect(result.version).toBe('minor'); + expect(result.newVersion?.version).toBe('1.1.0'); + }); + }); + + describe('Property changes classification', () => { + test('property-added is non-breaking (minor)', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + }; + const target: ResolvedSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('property-removed is breaking (major)', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }; + const target: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('required-added is breaking', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + }; + const target: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('required-removed is non-breaking', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'], + }; + const target: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + }); + + describe('Enum changes classification', () => { + test('enum-value-added is non-breaking', () => { + const source: ResolvedSchema = { type: 'string', enum: ['a', 'b'] }; + const target: ResolvedSchema = { type: 'string', enum: ['a', 'b', 'c'] }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('enum-value-removed is breaking', () => { + const source: ResolvedSchema = { type: 'string', enum: ['a', 'b', 'c'] }; + const target: ResolvedSchema = { type: 'string', enum: ['a', 'b'] }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('enum-added is breaking', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'string', enum: ['a', 'b'] }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('enum-removed is non-breaking', () => { + const source: ResolvedSchema = { type: 'string', enum: ['a', 'b'] }; + const target: ResolvedSchema = { type: 'string' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + }); + + describe('Constraint changes classification', () => { + test('constraint-tightened is breaking', () => { + const source: ResolvedSchema = { type: 'number', minimum: 0 }; + const target: ResolvedSchema = { type: 'number', minimum: 10 }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('constraint-loosened is non-breaking', () => { + const source: ResolvedSchema = { type: 'number', minimum: 10 }; + const target: ResolvedSchema = { type: 'number', minimum: 0 }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('minItems-increased is breaking', () => { + const source: ResolvedSchema = { type: 'array', items: { type: 'string' }, minItems: 1 }; + const target: ResolvedSchema = { type: 'array', items: { type: 'string' }, minItems: 5 }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('maxItems-decreased is breaking', () => { + const source: ResolvedSchema = { type: 'array', items: { type: 'string' }, maxItems: 10 }; + const target: ResolvedSchema = { type: 'array', items: { type: 'string' }, maxItems: 5 }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + }); + + describe('additionalProperties changes', () => { + test('additionalProperties denied is breaking', () => { + const source: ResolvedSchema = { type: 'object', properties: { name: { type: 'string' } } }; + const target: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + additionalProperties: false, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('additionalProperties allowed is non-breaking', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { name: { type: 'string' } }, + additionalProperties: false, + }; + const target: ResolvedSchema = { type: 'object', properties: { name: { type: 'string' } } }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + }); + + describe('Metadata changes are patch', () => { + test('description-changed is patch', () => { + const source: ResolvedSchema = { type: 'string', description: 'Old description' }; + const target: ResolvedSchema = { type: 'string', description: 'New description' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + + test('title-changed is patch', () => { + const source: ResolvedSchema = { type: 'string', title: 'Old title' }; + const target: ResolvedSchema = { type: 'string', title: 'New title' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + + test('default-changed is patch', () => { + const source: ResolvedSchema = { type: 'string', default: 'old' }; + const target: ResolvedSchema = { type: 'string', default: 'new' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + + test('examples-changed is patch', () => { + const source: ResolvedSchema = { type: 'string', examples: ['old'] }; + const target: ResolvedSchema = { type: 'string', examples: ['new'] }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + }); + + describe('Annotation changes are patch (Draft 2019-09+)', () => { + test('deprecated-changed is patch', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'string', deprecated: true }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + + test('readOnly-changed is patch', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'string', readOnly: true }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + + test('writeOnly-changed is patch', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'string', writeOnly: true }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('patch'); + }); + }); + + describe('Composition changes (anyOf/oneOf/allOf)', () => { + test('anyOf option added is breaking', () => { + const source: ResolvedSchema = { + anyOf: [{ type: 'string' }, { type: 'number' }], + }; + const target: ResolvedSchema = { + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('anyOf option removed is non-breaking', () => { + const source: ResolvedSchema = { + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }; + const target: ResolvedSchema = { + anyOf: [{ type: 'string' }, { type: 'number' }], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('oneOf option added is breaking', () => { + const source: ResolvedSchema = { + oneOf: [{ type: 'string' }, { type: 'number' }], + }; + const target: ResolvedSchema = { + oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('oneOf option removed is non-breaking', () => { + const source: ResolvedSchema = { + oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }; + const target: ResolvedSchema = { + oneOf: [{ type: 'string' }, { type: 'number' }], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('allOf member added is breaking', () => { + const source: ResolvedSchema = { + allOf: [{ type: 'object', properties: { a: { type: 'string' } } }], + }; + const target: ResolvedSchema = { + allOf: [ + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', properties: { b: { type: 'number' } } }, + ], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('allOf member removed is non-breaking', () => { + const source: ResolvedSchema = { + allOf: [ + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', properties: { b: { type: 'number' } } }, + ], + }; + const target: ResolvedSchema = { + allOf: [{ type: 'object', properties: { a: { type: 'string' } } }], + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + + test('not schema changed is breaking', () => { + const source: ResolvedSchema = { not: { type: 'string' } }; + const target: ResolvedSchema = { not: { type: 'number' } }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + }); + + describe('Draft 2020-12 keywords', () => { + test('dependentRequired-added is breaking', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { + creditCard: { type: 'string' }, + billingAddress: { type: 'string' }, + }, + }; + const target: ResolvedSchema = { + type: 'object', + properties: { + creditCard: { type: 'string' }, + billingAddress: { type: 'string' }, + }, + dependentRequired: { + creditCard: ['billingAddress'], + }, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('major'); + }); + + test('dependentRequired-removed is non-breaking', () => { + const source: ResolvedSchema = { + type: 'object', + properties: { + creditCard: { type: 'string' }, + billingAddress: { type: 'string' }, + }, + dependentRequired: { + creditCard: ['billingAddress'], + }, + }; + const target: ResolvedSchema = { + type: 'object', + properties: { + creditCard: { type: 'string' }, + billingAddress: { type: 'string' }, + }, + }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.version).toBe('minor'); + }); + }); + + describe('Strands trace format', () => { + test('traces have correct format', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.traces.length).toBeGreaterThan(0); + + const validCompatibilities: StrandsCompatibility[] = [ + 'incompatible', + 'compatible', + 'unknown', + ]; + for (const trace of result.traces) { + // Verify trace conforms to StrandsTrace interface + const typedTrace: StrandsTrace = trace; + expect(typedTrace).toHaveProperty('compatibility'); + expect(typedTrace).toHaveProperty('left'); + expect(typedTrace).toHaveProperty('right'); + expect(validCompatibilities).toContain(typedTrace.compatibility); + } + }); + + test('breaking changes have incompatible traces', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + + const result: CompareResult = compareSchemas(source, target); + const hasIncompatible = result.traces.some( + (t: StrandsTrace) => t.compatibility === 'incompatible' + ); + expect(hasIncompatible).toBe(true); + }); + + test('non-breaking changes have compatible traces', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: ['string', 'number'] }; + + const result: CompareResult = compareSchemas(source, target); + const hasCompatible = result.traces.some( + (t: StrandsTrace) => t.compatibility === 'compatible' + ); + expect(hasCompatible).toBe(true); + }); + }); + + describe('newVersion computation', () => { + test('computes newVersion correctly for major bump', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + const options: CompareOptions = { currentVersion: '1.2.3' }; + + const result: CompareResult = compareSchemas(source, target, options); + const expectedVersion: SemanticVersion = { + major: 2, + minor: 0, + patch: 0, + version: '2.0.0', + }; + expect(result.newVersion).toEqual(expectedVersion); + }); + + test('computes newVersion correctly for minor bump', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: ['string', 'number'] }; + const options: CompareOptions = { currentVersion: '1.2.3' }; + + const result: CompareResult = compareSchemas(source, target, options); + const expectedVersion: SemanticVersion = { + major: 1, + minor: 3, + patch: 0, + version: '1.3.0', + }; + expect(result.newVersion).toEqual(expectedVersion); + }); + + test('computes newVersion correctly for patch bump', () => { + const source: ResolvedSchema = { type: 'string', description: 'old' }; + const target: ResolvedSchema = { type: 'string', description: 'new' }; + const options: CompareOptions = { currentVersion: '1.2.3' }; + + const result: CompareResult = compareSchemas(source, target, options); + const expectedVersion: SemanticVersion = { + major: 1, + minor: 2, + patch: 4, + version: '1.2.4', + }; + expect(result.newVersion).toEqual(expectedVersion); + }); + + test('handles v-prefix in currentVersion', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + const options: CompareOptions = { currentVersion: 'v1.2.3' }; + + const result: CompareResult = compareSchemas(source, target, options); + expect(result.newVersion?.version).toBe('2.0.0'); + }); + + test('newVersion is null when currentVersion not provided', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + + const result: CompareResult = compareSchemas(source, target); + expect(result.newVersion).toBeNull(); + }); + }); + + describe('checkCompatibility helper', () => { + test('returns incompatible for breaking changes', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: 'number' }; + + const compatibility: StrandsCompatibility = checkCompatibility(source, target); + expect(compatibility).toBe('incompatible'); + }); + + test('returns compatible for non-breaking changes', () => { + const source: ResolvedSchema = { type: 'string' }; + const target: ResolvedSchema = { type: ['string', 'number'] }; + + const compatibility: StrandsCompatibility = checkCompatibility(source, target); + expect(compatibility).toBe('compatible'); + }); + + test('returns compatible for patch changes', () => { + const source: ResolvedSchema = { type: 'string', description: 'old' }; + const target: ResolvedSchema = { type: 'string', description: 'new' }; + + const compatibility: StrandsCompatibility = checkCompatibility(source, target); + expect(compatibility).toBe('compatible'); + }); + + test('returns compatible for identical schemas', () => { + const schema: ResolvedSchema = { type: 'string' }; + + const compatibility: StrandsCompatibility = checkCompatibility(schema, schema); + expect(compatibility).toBe('compatible'); + }); + }); +}); diff --git a/tests/e2e/15-json-schema-lint-rules.test.ts b/tests/e2e/15-json-schema-lint-rules.test.ts new file mode 100644 index 0000000..a9165d6 --- /dev/null +++ b/tests/e2e/15-json-schema-lint-rules.test.ts @@ -0,0 +1,838 @@ +/** + * JSON Schema Lint Rules Tests + * + * Tests the linting rules for JSON Schema quality and best practices. + * Based on Sourcemeta and JSON Schema community guidelines. + * + * @see https://github.com/sourcemeta/jsonschema + * @see https://github.com/orgs/json-schema-org/discussions/323 + */ + +import { describe, test, expect } from 'vitest'; +import { + runLintRules, + getAvailableRules, + getRuleDescription, + LINT_RULES, +} from '../../packages/governance/linters/json-schema-rules.js'; +import { lintJsonSchemaObject } from '../../packages/governance/linters/json-schema-ajv.js'; +import type { SchemaNode } from '../../packages/governance/linters/json-schema-rules.js'; +import type { LintIssue } from '../../packages/types/governance.js'; + +describe('JSON Schema Lint Rules', () => { + describe('Rule registry', () => { + test('all rules are registered', () => { + const ruleIds = getAvailableRules(); + expect(ruleIds.length).toBeGreaterThan(15); + }); + + test('each rule has a description', () => { + for (const rule of LINT_RULES) { + expect(rule.description).toBeTruthy(); + expect(getRuleDescription(rule.id)).toBe(rule.description); + } + }); + + test('each rule has a valid severity', () => { + for (const rule of LINT_RULES) { + expect(['error', 'warning']).toContain(rule.severity); + } + }); + }); + + describe('Schema declaration rules', () => { + test('missing-schema: warns when $schema is missing at root', () => { + const schema: SchemaNode = { + type: 'object', + properties: { name: { type: 'string' } }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-schema'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('warning'); + expect(issue?.path).toBe('/'); + }); + + test('missing-schema: no warning when $schema is present', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-schema'); + expect(issue).toBeUndefined(); + }); + + test('schema-not-at-root: warns when $schema appears in subschema without $id', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + nested: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string', + }, + }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'schema-not-at-root'); + expect(issue).toBeDefined(); + expect(issue?.path).toContain('nested'); + }); + + test('schema-not-at-root: no warning when subschema has $id', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + nested: { + $id: 'https://example.com/nested', + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string', + }, + }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'schema-not-at-root'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Metadata rules', () => { + test('missing-title: warns when root schema has no title', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-title'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('warning'); + }); + + test('missing-title: no warning when title is present', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'My Schema', + type: 'string', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-title'); + expect(issue).toBeUndefined(); + }); + + test('missing-description: warns when root schema has no description', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'string', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-description'); + expect(issue).toBeDefined(); + }); + + test('missing-description: no warning when description is present', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + description: 'A string value', + type: 'string', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'missing-description'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Enum/Const rules', () => { + test('enum-to-const: warns when enum has single value', () => { + const schema: SchemaNode = { + type: 'string', + enum: ['only-value'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'enum-to-const'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('const'); + }); + + test('enum-to-const: no warning when enum has multiple values', () => { + const schema: SchemaNode = { + type: 'string', + enum: ['a', 'b', 'c'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'enum-to-const'); + expect(issue).toBeUndefined(); + }); + + test('enum-with-type: warns when type is redundant with enum', () => { + const schema: SchemaNode = { + type: 'string', + enum: ['a', 'b', 'c'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'enum-with-type'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('redundant'); + }); + + test('enum-with-type: no warning when enum has mixed types', () => { + const schema: SchemaNode = { + type: ['string', 'number'], + enum: ['a', 1, 'b', 2], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'enum-with-type'); + // Should still warn because enum constrains to the same types + expect(issue).toBeDefined(); + }); + + test('const-with-type: warns when type is redundant with const', () => { + const schema: SchemaNode = { + type: 'string', + const: 'fixed-value', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'const-with-type'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('redundant'); + }); + + test('const-with-type: no warning when type differs from const', () => { + const schema: SchemaNode = { + type: ['string', 'null'], + const: 'fixed-value', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'const-with-type'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Conditional schema rules', () => { + test('if-without-then-else: warns when if has no then or else', () => { + const schema: SchemaNode = { + type: 'object', + if: { properties: { type: { const: 'a' } } }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'if-without-then-else'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('no effect'); + }); + + test('if-without-then-else: no warning when if has then', () => { + const schema: SchemaNode = { + type: 'object', + if: { properties: { type: { const: 'a' } } }, + then: { required: ['extra'] }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'if-without-then-else'); + expect(issue).toBeUndefined(); + }); + + test('if-without-then-else: no warning when if has else', () => { + const schema: SchemaNode = { + type: 'object', + if: { properties: { type: { const: 'a' } } }, + else: { required: ['other'] }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'if-without-then-else'); + expect(issue).toBeUndefined(); + }); + + test('then-else-without-if: warns when then appears without if', () => { + const schema: SchemaNode = { + type: 'object', + then: { required: ['extra'] }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'then-else-without-if'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('then'); + }); + + test('then-else-without-if: warns when else appears without if', () => { + const schema: SchemaNode = { + type: 'object', + else: { required: ['other'] }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'then-else-without-if'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('else'); + }); + }); + + describe('Array constraint rules', () => { + test('additional-items-redundant: warns when additionalItems with schema items', () => { + const schema: SchemaNode = { + type: 'array', + items: { type: 'string' }, + additionalItems: { type: 'number' }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'additional-items-redundant'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('ignored'); + }); + + test('additional-items-redundant: no warning when items is tuple', () => { + const schema: SchemaNode = { + type: 'array', + items: [{ type: 'string' }, { type: 'number' }], + additionalItems: { type: 'boolean' }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'additional-items-redundant'); + expect(issue).toBeUndefined(); + }); + + test('contains-required: warns when minContains without contains', () => { + const schema: SchemaNode = { + type: 'array', + minContains: 2, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'contains-required'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('minContains'); + }); + + test('contains-required: warns when maxContains without contains', () => { + const schema: SchemaNode = { + type: 'array', + maxContains: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'contains-required'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('maxContains'); + }); + + test('contains-required: no warning when contains is present', () => { + const schema: SchemaNode = { + type: 'array', + contains: { type: 'string' }, + minContains: 2, + maxContains: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'contains-required'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Type compatibility rules', () => { + test('type-incompatible-keywords: warns when string keywords on number type', () => { + const schema: SchemaNode = { + type: 'number', + minLength: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'type-incompatible-keywords'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('minLength'); + expect(issue?.message).toContain('string'); + }); + + test('type-incompatible-keywords: warns when array keywords on object type', () => { + const schema: SchemaNode = { + type: 'object', + minItems: 1, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'type-incompatible-keywords'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('minItems'); + expect(issue?.message).toContain('array'); + }); + + test('type-incompatible-keywords: warns when numeric keywords on string type', () => { + const schema: SchemaNode = { + type: 'string', + minimum: 0, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'type-incompatible-keywords'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('minimum'); + }); + + test('type-incompatible-keywords: no warning when keywords match type', () => { + const schema: SchemaNode = { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '^[a-z]+$', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'type-incompatible-keywords'); + expect(issue).toBeUndefined(); + }); + + test('type-incompatible-keywords: no check when type is array (multiple types)', () => { + const schema: SchemaNode = { + type: ['string', 'number'], + minLength: 1, + minimum: 0, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'type-incompatible-keywords'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Range validation rules', () => { + test('invalid-numeric-range: errors when maximum < minimum', () => { + const schema: SchemaNode = { + type: 'number', + minimum: 100, + maximum: 10, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-numeric-range'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + expect(issue?.message).toContain('maximum'); + expect(issue?.message).toContain('minimum'); + }); + + test('invalid-numeric-range: no error when maximum >= minimum', () => { + const schema: SchemaNode = { + type: 'number', + minimum: 0, + maximum: 100, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-numeric-range'); + expect(issue).toBeUndefined(); + }); + + test('invalid-numeric-range: errors when exclusiveMaximum <= exclusiveMinimum', () => { + const schema: SchemaNode = { + type: 'number', + exclusiveMinimum: 10, + exclusiveMaximum: 10, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-numeric-range'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('exclusiveMaximum'); + }); + + test('invalid-length-range: errors when maxLength < minLength', () => { + const schema: SchemaNode = { + type: 'string', + minLength: 10, + maxLength: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-length-range'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + }); + + test('invalid-items-range: errors when maxItems < minItems', () => { + const schema: SchemaNode = { + type: 'array', + minItems: 10, + maxItems: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-items-range'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + }); + + test('invalid-properties-range: errors when maxProperties < minProperties', () => { + const schema: SchemaNode = { + type: 'object', + minProperties: 10, + maxProperties: 5, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-properties-range'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + }); + }); + + describe('Format rules', () => { + test('unknown-format: warns for unknown format values', () => { + const schema: SchemaNode = { + type: 'string', + format: 'custom-format', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'unknown-format'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('custom-format'); + }); + + test('unknown-format: no warning for known formats', () => { + const knownFormats = [ + 'date-time', + 'date', + 'time', + 'email', + 'uri', + 'uuid', + 'ipv4', + 'ipv6', + 'hostname', + 'regex', + ]; + + for (const format of knownFormats) { + const schema: SchemaNode = { type: 'string', format }; + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'unknown-format'); + expect(issue).toBeUndefined(); + } + }); + }); + + describe('Empty schema rules', () => { + test('empty-enum: errors when enum is empty array', () => { + const schema: SchemaNode = { + type: 'string', + enum: [], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'empty-enum'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + expect(issue?.message).toContain('never validate'); + }); + + test('empty-required: warns when required is empty array', () => { + const schema: SchemaNode = { + type: 'object', + properties: { name: { type: 'string' } }, + required: [], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'empty-required'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('warning'); + }); + + test('empty-allof-anyof-oneof: errors when anyOf is empty', () => { + const schema: SchemaNode = { + anyOf: [], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'empty-allof-anyof-oneof'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + }); + + test('empty-allof-anyof-oneof: errors when oneOf is empty', () => { + const schema: SchemaNode = { + oneOf: [], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'empty-allof-anyof-oneof'); + expect(issue).toBeDefined(); + expect(issue?.severity).toBe('error'); + }); + + test('empty-allof-anyof-oneof: warns when allOf is empty', () => { + const schema: SchemaNode = { + allOf: [], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'empty-allof-anyof-oneof'); + expect(issue).toBeDefined(); + // allOf empty is just redundant, not invalid + expect(issue?.severity).toBe('warning'); + }); + }); + + describe('Required properties rules', () => { + test('required-undefined-property: warns when required property not in properties', () => { + const schema: SchemaNode = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name', 'email'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'required-undefined-property'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('email'); + }); + + test('required-undefined-property: no warning when all required are defined', () => { + const schema: SchemaNode = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + required: ['name', 'email'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'required-undefined-property'); + expect(issue).toBeUndefined(); + }); + + test('duplicate-required: warns when required has duplicates', () => { + const schema: SchemaNode = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name', 'name'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'duplicate-required'); + expect(issue).toBeDefined(); + expect(issue?.message).toContain('name'); + }); + + test('duplicate-required: no warning when required has no duplicates', () => { + const schema: SchemaNode = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + required: ['name', 'email'], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'duplicate-required'); + expect(issue).toBeUndefined(); + }); + }); + + describe('Nested schema linting', () => { + test('rules apply to nested properties', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Root', + description: 'Root schema', + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + value: { + type: 'number', + minimum: 100, + maximum: 10, // Invalid range + }, + }, + }, + }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-numeric-range'); + expect(issue).toBeDefined(); + expect(issue?.path).toContain('nested'); + expect(issue?.path).toContain('value'); + }); + + test('rules apply to items schema', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Root', + description: 'Root schema', + type: 'array', + items: { + type: 'string', + enum: ['only'], // Should suggest const + }, + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'enum-to-const'); + expect(issue).toBeDefined(); + expect(issue?.path).toContain('items'); + }); + + test('rules apply to allOf/anyOf/oneOf schemas', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Root', + description: 'Root schema', + anyOf: [ + { type: 'string', minLength: 10, maxLength: 5 }, // Invalid range + { type: 'number' }, + ], + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-length-range'); + expect(issue).toBeDefined(); + expect(issue?.path).toContain('anyOf'); + }); + + test('rules apply to $defs schemas', () => { + const schema: SchemaNode = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Root', + description: 'Root schema', + $defs: { + BadDef: { + type: 'array', + minItems: 10, + maxItems: 5, // Invalid range + }, + }, + $ref: '#/$defs/BadDef', + }; + + const issues = runLintRules(schema); + const issue = issues.find((i) => i.rule === 'invalid-items-range'); + expect(issue).toBeDefined(); + expect(issue?.path).toContain('$defs'); + }); + }); + + describe('Rule filtering', () => { + test('enabledRules filters to only specified rules', () => { + const schema: SchemaNode = { + type: 'string', + enum: ['only'], + minLength: 10, + maxLength: 5, + }; + + const enabledRules = new Set(['enum-to-const']); + const issues = runLintRules(schema, enabledRules); + + expect(issues.length).toBe(1); + expect(issues[0].rule).toBe('enum-to-const'); + }); + + test('disabledRules excludes specified rules', () => { + const schema: SchemaNode = { + type: 'string', + enum: ['only'], + }; + + const disabledRules = new Set(['enum-to-const', 'enum-with-type']); + const issues = runLintRules(schema, undefined, disabledRules); + + const enumIssues = issues.filter( + (i) => i.rule === 'enum-to-const' || i.rule === 'enum-with-type' + ); + expect(enumIssues).toHaveLength(0); + }); + }); + + describe('Full linter integration (lintJsonSchemaObject)', () => { + test('combines meta-validation and style rules', async () => { + const schema: SchemaNode = { + type: 'objekt', // Invalid type value (meta-validation error) + enum: ['only'], // Style warning + }; + + const result = await lintJsonSchemaObject(schema); + + // Should have meta-validation error + expect(result.errors.length).toBeGreaterThan(0); + + // Should have style warnings + expect(result.warnings.length).toBeGreaterThan(0); + }); + + test('skipMetaValidation option works', async () => { + const schema: SchemaNode = { + type: 'objekt', // Invalid but should be skipped + }; + + const result = await lintJsonSchemaObject(schema, { skipMetaValidation: true }); + + // Should not have meta-validation errors + const metaErrors = result.errors.filter((e) => e.rule?.startsWith('meta:')); + expect(metaErrors).toHaveLength(0); + }); + + test('skipStyleRules option works', async () => { + const schema: SchemaNode = { + type: 'string', + enum: ['only'], // Should be skipped + }; + + const result = await lintJsonSchemaObject(schema, { skipStyleRules: true }); + + // Should not have style rule warnings + const styleWarnings = result.warnings.filter((w) => w.rule === 'enum-to-const'); + expect(styleWarnings).toHaveLength(0); + }); + + test('valid schema with best practices passes all checks', async () => { + const schema: SchemaNode = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'User', + description: 'A user object', + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + name: { type: 'string', minLength: 1, maxLength: 100 }, + email: { type: 'string', format: 'email' }, + age: { type: 'integer', minimum: 0, maximum: 150 }, + }, + required: ['id', 'name', 'email'], + additionalProperties: false, + }; + + const result = await lintJsonSchemaObject(schema); + + expect(result.errors).toHaveLength(0); + // May have some warnings (like missing-description on nested props) + }); + }); +}); diff --git a/tests/e2e/16-diff-command.test.ts b/tests/e2e/16-diff-command.test.ts new file mode 100644 index 0000000..3a217de --- /dev/null +++ b/tests/e2e/16-diff-command.test.ts @@ -0,0 +1,633 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual diff', () => { + describe('basic functionality', () => { + test('shows "no changes" when specs match snapshots', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Create snapshot matching current spec + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no changes/i); + } finally { + cleanup(); + } + }); + + test('shows classified changes when specs differ from snapshots', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Current spec has a field removed (breaking change) + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + // Snapshot is the base version + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order-schema/i); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + + test('always exits 0 on success regardless of breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Breaking change: field removed + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff', dir); + + // diff always exits 0 (unlike breaking which exits 1) + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + }); + + describe('options', () => { + test('--contract filters to single contract', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-schema', + type: 'json-schema', + path: 'schemas/user.json', + }, + ]); + + // Both have changes + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/user.json') + ); + + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/user-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'user-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --contract order-schema', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order-schema/i); + expect(result.stdout).not.toMatch(/user-schema/i); + } finally { + cleanup(); + } + }); + + test('--format json outputs valid JSON', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --format json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(output).toHaveProperty('contracts'); + expect(typeof output.contracts).toBe('object'); + + const contractNames = Object.keys(output.contracts); + if (contractNames.length > 0) { + const firstContract = output.contracts[contractNames[0]]; + expect(firstContract).toHaveProperty('changes'); + expect(firstContract).toHaveProperty('summary'); + expect(firstContract).toHaveProperty('suggestedBump'); + } + } finally { + cleanup(); + } + }); + + test('--severity breaking filters to only breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Use a spec that has both breaking and non-breaking changes + // For this we need a spec with field removed (breaking) and description changed (patch) + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --severity breaking --format json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + // All changes in contracts should be breaking + for (const contractName in output.contracts) { + const contract = output.contracts[contractName]; + for (const change of contract.changes) { + expect(change.severity).toBe('breaking'); + } + } + } finally { + cleanup(); + } + }); + + test('--severity non-breaking filters to only non-breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Non-breaking: optional field added + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --severity non-breaking --format json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + // All changes should be non-breaking + for (const contractName in output.contracts) { + const contract = output.contracts[contractName]; + for (const change of contract.changes) { + expect(change.severity).toBe('non-breaking'); + } + } + } finally { + cleanup(); + } + }); + + test('--verbose shows JSON Pointer paths', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff --verbose', dir); + + expect(result.exitCode).toBe(0); + // Verbose output should include "path:" lines + expect(result.stdout).toMatch(/path:/i); + } finally { + cleanup(); + } + }); + }); + + describe('edge cases', () => { + test('shows "no changes" for first version (no snapshot)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // No snapshot exists - first version + + const result = run('diff', dir); + + expect(result.exitCode).toBe(0); + // Should indicate first version or no changes + expect(result.stdout).toMatch(/no changes|first version|no snapshot/i); + } finally { + cleanup(); + } + }); + + test('exits with error on configuration error', () => { + const { dir, cleanup } = createTempRepo(); + try { + // No contractual.yaml - should fail + + const result = run('diff', dir, { expectFail: true }); + + expect(result.exitCode).toBeGreaterThan(0); + expect(result.stdout + result.stderr).toMatch(/init|config|not found/i); + } finally { + cleanup(); + } + }); + + test('handles contract with breaking detection disabled', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + breaking: false, // Disabled + }, + ]); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff', dir); + + expect(result.exitCode).toBe(0); + // Should indicate skipped or disabled + expect(result.stdout).toMatch(/disabled|skipped|no changes/i); + } finally { + cleanup(); + } + }); + + test('reports error for non-existent contract name', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('diff --contract nonexistent', dir, { expectFail: true }); + + expect(result.exitCode).toBeGreaterThan(0); + expect(result.stdout + result.stderr).toMatch(/not found|nonexistent/i); + } finally { + cleanup(); + } + }); + }); + + describe('OpenAPI contracts', () => { + test('shows changes for OpenAPI specs', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore-api', + type: 'openapi', + path: 'specs/petstore.yaml', + lint: false, // Disable linting for petstore fixture + }, + ]); + + // Current spec has endpoint removed (breaking) + copyFixture( + 'openapi/petstore-breaking-endpoint-removed.yaml', + path.join(dir, 'specs/petstore.yaml') + ); + + // Snapshot is base version + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore-api.yaml') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'petstore-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('diff', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/petstore-api/i); + expect(result.stdout).toMatch(/breaking/i); + } finally { + cleanup(); + } + }); + }); +}); + +describe('breaking command after refactor', () => { + test('still exits 1 on breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + } finally { + cleanup(); + } + }); + + test('still exits 0 when no breaking changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Non-breaking change only + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('breaking', dir); + + expect(result.exitCode).toBe(0); + } finally { + cleanup(); + } + }); +}); + +describe('changeset command after refactor', () => { + test('still generates changeset from detected changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('changeset', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/created changeset/i); + } finally { + cleanup(); + } + }); + + test('still shows "no changes" when specs match', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('changeset', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/no change/i); + } finally { + cleanup(); + } + }); +}); diff --git a/tests/e2e/17-pre-release.test.ts b/tests/e2e/17-pre-release.test.ts new file mode 100644 index 0000000..76069b2 --- /dev/null +++ b/tests/e2e/17-pre-release.test.ts @@ -0,0 +1,403 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + fileExists, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual pre', () => { + describe('pre enter', () => { + test('enters pre-release mode with tag', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const result = run('pre enter beta', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/entered pre-release mode/i); + expect(result.stdout).toMatch(/beta/i); + expect(fileExists(dir, '.contractual/pre.json')).toBe(true); + + const preState = readJSON(dir, '.contractual/pre.json') as { + tag: string; + enteredAt: string; + initialVersions: Record; + }; + expect(preState.tag).toBe('beta'); + expect(preState.initialVersions['order-schema']).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + + test('creates pre.json with correct structure', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'api', + type: 'json-schema', + path: 'schemas/api.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/api.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '2.5.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + run('pre enter alpha', dir); + + const preState = readJSON(dir, '.contractual/pre.json') as { + tag: string; + enteredAt: string; + initialVersions: Record; + }; + + expect(preState.tag).toBe('alpha'); + expect(preState.enteredAt).toBeDefined(); + expect(new Date(preState.enteredAt).getTime()).not.toBeNaN(); + expect(preState.initialVersions).toEqual({ api: '2.5.0' }); + } finally { + cleanup(); + } + }); + + test('fails if already in pre-release mode', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Enter pre-release mode first + run('pre enter beta', dir); + + // Try to enter again + const result = run('pre enter alpha', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/already in pre-release mode/i); + } finally { + cleanup(); + } + }); + + test('fails if not initialized', () => { + const { dir, cleanup } = createTempRepo(); + try { + // No contractual.yaml or .contractual directory + + const result = run('pre enter beta', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/no .contractual directory|init/i); + } finally { + cleanup(); + } + }); + }); + + describe('pre exit', () => { + test('exits pre-release mode', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Enter and then exit + run('pre enter beta', dir); + expect(fileExists(dir, '.contractual/pre.json')).toBe(true); + + const result = run('pre exit', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/exited pre-release mode/i); + expect(fileExists(dir, '.contractual/pre.json')).toBe(false); + } finally { + cleanup(); + } + }); + + test('shows message if not in pre-release mode', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('pre exit', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/not in pre-release mode/i); + } finally { + cleanup(); + } + }); + + test('fails if not initialized', () => { + const { dir, cleanup } = createTempRepo(); + try { + const result = run('pre exit', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/no .contractual directory|init/i); + } finally { + cleanup(); + } + }); + }); + + describe('pre status', () => { + test('shows current pre-release tag and entry time', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + run('pre enter rc', dir); + + const result = run('pre status', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/rc/i); + expect(result.stdout).toMatch(/tag|pre-release/i); + } finally { + cleanup(); + } + }); + + test('shows "not in pre-release mode" when inactive', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('pre status', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/not in pre-release mode/i); + } finally { + cleanup(); + } + }); + + test('fails if not initialized', () => { + const { dir, cleanup } = createTempRepo(); + try { + const result = run('pre status', dir, { expectFail: true }); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/no .contractual directory|init/i); + } finally { + cleanup(); + } + }); + }); + + describe('version with pre-release', () => { + test('bumps to pre-release version', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Setup initial state + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture('json-schema/order-field-removed.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Enter pre-release mode + run('pre enter beta', dir); + + // Create changeset + const changeset = `--- +"order-schema": major +--- + +Breaking change for beta testing +`; + writeFile(dir, '.contractual/changesets/breaking.md', changeset); + + // Run version + const result = run('version --yes', dir); + + expect(result.exitCode).toBe(0); + + // Check version is pre-release format + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toMatch(/2\.0\.0-beta/); + } finally { + cleanup(); + } + }); + + test('increments pre-release number on subsequent versions', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + + // Start with a pre-release version already + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + copyFixture( + 'json-schema/order-optional-field-added.json', + path.join(dir, 'schemas/order.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '2.0.0-beta.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create pre.json to simulate being in pre-release mode + writeFile( + dir, + '.contractual/pre.json', + JSON.stringify({ + tag: 'beta', + enteredAt: '2026-01-01T00:00:00Z', + initialVersions: { 'order-schema': '1.0.0' }, + }) + ); + + // Create changeset + const changeset = `--- +"order-schema": minor +--- + +Another beta change +`; + writeFile(dir, '.contractual/changesets/minor.md', changeset); + + // Run version + const result = run('version --yes', dir); + + expect(result.exitCode).toBe(0); + + // Check version incremented pre-release number + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + // Should be 2.1.0-beta.0 or 2.0.0-beta.1 depending on implementation + expect(versions['order-schema'].version).toMatch(/beta/); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/18-contract-management.test.ts b/tests/e2e/18-contract-management.test.ts new file mode 100644 index 0000000..c7305a4 --- /dev/null +++ b/tests/e2e/18-contract-management.test.ts @@ -0,0 +1,312 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + readYAML, + fileExists, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual contract', () => { + describe('contract add', () => { + test('adds contract to existing config', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Initialize with one contract + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Add a new spec file + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Add contract using CLI + const result = run( + 'contract add --name user-schema --type json-schema --path schemas/user.json --initial-version 0.0.0 -y', + dir + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/added.*user-schema/i); + + // Verify config was updated + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ name: string; type: string; path: string }>; + }; + expect(config.contracts).toHaveLength(2); + expect(config.contracts.find((c) => c.name === 'user-schema')).toBeDefined(); + } finally { + cleanup(); + } + }); + + test('creates snapshot and updates versions.json', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Add new contract + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/new-api.json')); + + const result = run( + 'contract add --name new-api --type json-schema --path schemas/new-api.json --initial-version 0.5.0 -y', + dir + ); + + expect(result.exitCode).toBe(0); + + // Verify snapshot created + expect(fileExists(dir, '.contractual/snapshots/new-api.json')).toBe(true); + + // Verify versions.json updated + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['new-api'].version).toBe('0.5.0'); + expect(versions['order-schema'].version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + + test('validates spec file type', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Create a non-matching spec file (JSON Schema file but claim it's OpenAPI) + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/fake-api.json')); + + const result = run( + 'contract add --name fake-api --type openapi --path schemas/fake-api.json -y', + dir, + { expectFail: true } + ); + + expect(result.exitCode).toBe(1); + expect(result.stdout).toMatch(/type mismatch|invalid|detected/i); + } finally { + cleanup(); + } + }); + + test('--skip-validation bypasses type check', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Create file + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/custom.json')); + + const result = run( + 'contract add --name custom-api --type openapi --path schemas/custom.json --skip-validation -y', + dir + ); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/added.*custom-api/i); + } finally { + cleanup(); + } + }); + + test('fails if not initialized', () => { + const { dir, cleanup } = createTempRepo(); + try { + // No contractual.yaml + + const result = run( + 'contract add --name test --type json-schema --path test.json -y', + dir, + { expectFail: true } + ); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/not initialized|init/i); + } finally { + cleanup(); + } + }); + + test('fails if contract name already exists', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + // Try to add with same name + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/other.json')); + + const result = run( + 'contract add --name order-schema --type json-schema --path schemas/other.json -y', + dir, + { expectFail: true } + ); + + expect(result.exitCode).toBe(1); + expect(result.stdout + result.stderr).toMatch(/contract exists|already defined|duplicate/i); + } finally { + cleanup(); + } + }); + }); + + describe('contract list', () => { + test('lists all contracts', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-api', + type: 'openapi', + path: 'specs/user.yaml', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/user.yaml')); + + const result = run('contract list', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).toMatch(/user-api/); + expect(result.stdout).toMatch(/json-schema/); + expect(result.stdout).toMatch(/openapi/); + } finally { + cleanup(); + } + }); + + test('filters by exact name', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + { + name: 'user-schema', + type: 'json-schema', + path: 'schemas/user.json', + }, + { + name: 'product-api', + type: 'openapi', + path: 'specs/product.yaml', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/product.yaml')); + + const result = run('contract list order-schema', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).not.toMatch(/user-schema/); + expect(result.stdout).not.toMatch(/product-api/); + } finally { + cleanup(); + } + }); + + test('--json outputs structured JSON', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + const result = run('contract list --json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(Array.isArray(output)).toBe(true); + expect(output[0].name).toBe('order-schema'); + expect(output[0].type).toBe('json-schema'); + } finally { + cleanup(); + } + }); + + test('shows message when no contracts (schema requires at least one)', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Schema validation requires at least one contract, so empty array fails + writeFile(dir, 'contractual.yaml', 'contracts: []\n'); + + const result = run('contract list', dir, { expectFail: true }); + + // Schema validation fails on empty contracts array + expect(result.exitCode).toBeGreaterThan(0); + expect(result.stdout + result.stderr).toMatch(/must NOT have fewer than 1 items|invalid|contracts/i); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/19-init-enhanced.test.ts b/tests/e2e/19-init-enhanced.test.ts new file mode 100644 index 0000000..074ebc4 --- /dev/null +++ b/tests/e2e/19-init-enhanced.test.ts @@ -0,0 +1,240 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + writeFile, + readJSON, + readYAML, + fileExists, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual init (enhanced)', () => { + describe('version options', () => { + test('--initial-version sets starting version', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create a spec file + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + const result = run('init --initial-version 1.5.0 -y', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/v1\.5\.0/); + + // Verify versions.json + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + const contractName = Object.keys(versions)[0]; + expect(versions[contractName].version).toBe('1.5.0'); + } finally { + cleanup(); + } + }); + + test('-y uses default version (0.0.0)', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + const result = run('init -y', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/v0\.0\.0/); + + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + const contractName = Object.keys(versions)[0]; + expect(versions[contractName].version).toBe('0.0.0'); + } finally { + cleanup(); + } + }); + + test('creates snapshots at initial version', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + run('init --initial-version 2.0.0 -y', dir); + + // Verify snapshot was created + expect(fileExists(dir, '.contractual/snapshots')).toBe(true); + + // Find snapshot file + const config = readYAML(dir, 'contractual.yaml') as { + contracts: Array<{ name: string }>; + }; + const contractName = config.contracts[0].name; + + // Snapshot should exist (extension may vary) + const snapshotExists = + fileExists(dir, `.contractual/snapshots/${contractName}.json`) || + fileExists(dir, `.contractual/snapshots/${contractName}.yaml`); + expect(snapshotExists).toBe(true); + } finally { + cleanup(); + } + }); + }); + + describe('versioning modes', () => { + test('--versioning independent (default)', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + const result = run('init --versioning independent -y', dir); + + expect(result.exitCode).toBe(0); + + // Independent mode should not add versioning section (it's the default) + const config = readYAML(dir, 'contractual.yaml') as { + versioning?: { mode: string }; + }; + // Default mode doesn't need explicit config + expect(config.versioning?.mode).toBeUndefined(); + } finally { + cleanup(); + } + }); + + test('--versioning fixed', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + const result = run('init --versioning fixed -y', dir); + + expect(result.exitCode).toBe(0); + + const config = readYAML(dir, 'contractual.yaml') as { + versioning?: { mode: string }; + }; + expect(config.versioning?.mode).toBe('fixed'); + } finally { + cleanup(); + } + }); + }); + + describe('--force flag', () => { + test('reinitializes existing project', () => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'order.schema.json')); + + // First init + run('init --initial-version 1.0.0 -y', dir); + + // Verify first init + let versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + const contractName = Object.keys(versions)[0]; + expect(versions[contractName].version).toBe('1.0.0'); + + // Force reinit with different version + const result = run('init --initial-version 2.0.0 --force -y', dir); + + expect(result.exitCode).toBe(0); + + // Verify reinit + versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions[contractName].version).toBe('2.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('existing project handling', () => { + test('initializes unversioned contracts in existing project', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create config manually with two contracts + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: order-schema + type: json-schema + path: schemas/order.json + - name: user-schema + type: json-schema + path: schemas/user.json +` + ); + + // Create spec files + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/user.json')); + + // Create .contractual dir with only one contract versioned + writeFile(dir, '.contractual/versions.json', '{}'); + + // Run init - should detect unversioned contracts + const result = run('init -y', dir); + + expect(result.exitCode).toBe(0); + // Should mention finding unversioned contracts + expect(result.stdout).toMatch(/without version|uninitialized|initialized/i); + } finally { + cleanup(); + } + }); + + test('skips already versioned contracts', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create config + writeFile( + dir, + 'contractual.yaml', + `contracts: + - name: order-schema + type: json-schema + path: schemas/order.json +` + ); + + // Create spec and snapshot + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Run init - should recognize all contracts have snapshots + const result = run('init -y', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/already initialized|all contracts have snapshots/i); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/20-version-enhanced.test.ts b/tests/e2e/20-version-enhanced.test.ts new file mode 100644 index 0000000..7cd984b --- /dev/null +++ b/tests/e2e/20-version-enhanced.test.ts @@ -0,0 +1,453 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readJSON, + fileExists, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('contractual version (enhanced)', () => { + describe('--dry-run', () => { + test('shows preview without applying changes', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // Create changeset + const changeset = `--- +"order-schema": minor +--- + +Added new feature +`; + writeFile(dir, '.contractual/changesets/feature.md', changeset); + + const result = run('version --dry-run', dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/dry run|preview/i); + expect(result.stdout).toMatch(/order-schema/); + expect(result.stdout).toMatch(/1\.0\.0/); + expect(result.stdout).toMatch(/1\.1\.0|minor/); + } finally { + cleanup(); + } + }); + + test('does not modify versions.json', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": major +--- + +Breaking change +`; + writeFile(dir, '.contractual/changesets/breaking.md', changeset); + + run('version --dry-run', dir); + + // Verify version unchanged + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + + test('does not delete changesets', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": patch +--- + +Bug fix +`; + writeFile(dir, '.contractual/changesets/fix.md', changeset); + + run('version --dry-run', dir); + + // Verify changeset still exists + const changesets = listFiles(dir, '.contractual/changesets'); + expect(changesets).toContain('fix.md'); + } finally { + cleanup(); + } + }); + }); + + describe('--json', () => { + test('outputs structured JSON', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": minor +--- + +New feature +`; + writeFile(dir, '.contractual/changesets/feature.md', changeset); + + const result = run('version --json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(output.bumps).toBeDefined(); + expect(Array.isArray(output.bumps)).toBe(true); + + if (output.bumps.length > 0) { + expect(output.bumps[0].contract).toBe('order-schema'); + expect(output.bumps[0].old).toBe('1.0.0'); + expect(output.bumps[0].new).toBe('1.1.0'); + expect(output.bumps[0].type).toBe('minor'); + } + } finally { + cleanup(); + } + }); + + test('implies --yes (no prompts)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": minor +--- + +Feature +`; + writeFile(dir, '.contractual/changesets/feat.md', changeset); + + // --json should apply without prompting (implies --yes) + const result = run('version --json', dir); + + expect(result.exitCode).toBe(0); + + // Verify version was bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + + test('works with --dry-run', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": major +--- + +Breaking +`; + writeFile(dir, '.contractual/changesets/breaking.md', changeset); + + const result = run('version --json --dry-run', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(output.dryRun).toBe(true); + expect(output.bumps).toBeDefined(); + + // Verify version NOT changed + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('-y/--yes', () => { + test('skips confirmation prompt', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order-schema.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"order-schema": patch +--- + +Fix +`; + writeFile(dir, '.contractual/changesets/fix.md', changeset); + + const result = run('version --yes', dir); + + expect(result.exitCode).toBe(0); + + // Verify version was bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order-schema'].version).toBe('1.0.1'); + } finally { + cleanup(); + } + }); + + test('applies changes immediately', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'api-1', + type: 'json-schema', + path: 'schemas/api1.json', + }, + { + name: 'api-2', + type: 'json-schema', + path: 'schemas/api2.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/api1.json')); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/api2.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/api-1.json') + ); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/api-2.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'api-1': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'api-2': { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const changeset = `--- +"api-1": minor +"api-2": major +--- + +Multiple changes +`; + writeFile(dir, '.contractual/changesets/multi.md', changeset); + + const result = run('version -y', dir); + + expect(result.exitCode).toBe(0); + + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api-1'].version).toBe('1.1.0'); + expect(versions['api-2'].version).toBe('3.0.0'); + + // Verify changeset was consumed + const changesets = listFiles(dir, '.contractual/changesets'); + expect(changesets.filter((f) => f.endsWith('.md'))).toHaveLength(0); + } finally { + cleanup(); + } + }); + }); + + describe('no changesets', () => { + test('shows message when no changesets with --json', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'order-schema', + type: 'json-schema', + path: 'schemas/order.json', + }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'order-schema': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + // No changesets + + const result = run('version --json', dir); + + expect(result.exitCode).toBe(0); + + const output = JSON.parse(result.stdout); + expect(output.bumps).toHaveLength(0); + expect(output.changesets).toBe(0); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/21-spec-version-sync.test.ts b/tests/e2e/21-spec-version-sync.test.ts new file mode 100644 index 0000000..cada4b9 --- /dev/null +++ b/tests/e2e/21-spec-version-sync.test.ts @@ -0,0 +1,992 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import path from 'node:path'; +import { + createTempRepo, + copyFixture, + run, + setupRepoWithConfig, + writeFile, + readFile, + readJSON, + readYAML, + fileExists, + listFiles, + ensureCliBuilt, +} from './helpers.js'; + +beforeAll(() => { + ensureCliBuilt(); +}); + +describe('spec version sync', () => { + describe('OpenAPI (YAML)', () => { + test('updates info.version in spec on version bump', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'petstore', type: 'openapi', path: 'specs/petstore.yaml' }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + + // Copy to snapshot (simulating init) + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump-petstore.md', + `---\n"petstore": minor\n---\n\n## petstore\n- Added new endpoint\n` + ); + + const result = run('version', dir); + expect(result.exitCode).toBe(0); + + // Spec file should have updated version + const spec = readYAML(dir, 'specs/petstore.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.1.0'); + + // Snapshot should also have updated version + const snapshot = readYAML(dir, '.contractual/snapshots/petstore.yaml') as { + info: { version: string }; + }; + expect(snapshot.info.version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + + test('preserves YAML comments and formatting when updating version', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + // Write a spec with comments + writeFile( + dir, + 'specs/api.yaml', + `# My API spec +openapi: 3.0.3 +info: + title: My API + # Current version + version: 1.0.0 +paths: {} +` + ); + + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/api.yaml') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": patch\n---\n\n## api\n- Fixed bug\n` + ); + + run('version', dir); + + const content = readFile(dir, 'specs/api.yaml'); + // Version updated + expect(content).toContain('version: 1.0.1'); + // Comments preserved + expect(content).toContain('# My API spec'); + expect(content).toContain('# Current version'); + } finally { + cleanup(); + } + }); + }); + + describe('OpenAPI (JSON)', () => { + test('updates info.version in JSON spec on version bump', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.json' }, + ]); + + writeFile( + dir, + 'specs/api.json', + JSON.stringify( + { + openapi: '3.0.3', + info: { title: 'My API', version: '2.0.0' }, + paths: {}, + }, + null, + 2 + ) + '\n' + ); + + // Snapshot + writeFile( + dir, + '.contractual/snapshots/api.json', + JSON.stringify( + { + openapi: '3.0.3', + info: { title: 'My API', version: '2.0.0' }, + paths: {}, + }, + null, + 2 + ) + '\n' + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": major\n---\n\n## api\n- Breaking change\n` + ); + + run('version', dir); + + const spec = readJSON(dir, 'specs/api.json') as { info: { version: string } }; + expect(spec.info.version).toBe('3.0.0'); + + // Snapshot too + const snapshot = readJSON(dir, '.contractual/snapshots/api.json') as { + info: { version: string }; + }; + expect(snapshot.info.version).toBe('3.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('JSON Schema (no version field)', () => { + test('does not modify json-schema spec (no standard version field)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'order', type: 'json-schema', path: 'schemas/order.json' }, + ]); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const specBefore = readFile(dir, 'schemas/order.json'); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"order": minor\n---\n\n## order\n- Added field\n` + ); + + run('version', dir); + + // Spec content should be unchanged (no version field to update) + const specAfter = readFile(dir, 'schemas/order.json'); + expect(specAfter).toBe(specBefore); + + // But versions.json should still be bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['order'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + }); + + describe('syncVersion: false', () => { + test('does not update spec version when syncVersion is false', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { + name: 'petstore', + type: 'openapi', + path: 'specs/petstore.yaml', + syncVersion: false, + }, + ]); + copyFixture('openapi/petstore-base.yaml', path.join(dir, 'specs/petstore.yaml')); + copyFixture( + 'openapi/petstore-base.yaml', + path.join(dir, '.contractual/snapshots/petstore.yaml') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + petstore: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"petstore": minor\n---\n\n## petstore\n- Added endpoint\n` + ); + + run('version', dir); + + // Spec should keep original version (1.0.27 from the fixture) + const spec = readYAML(dir, 'specs/petstore.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.27'); + + // But versions.json should still be bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['petstore'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + + test('mixed: syncs one contract, skips another with syncVersion: false', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'synced-api', type: 'openapi', path: 'specs/synced.yaml' }, + { + name: 'unsynced-api', + type: 'openapi', + path: 'specs/unsynced.yaml', + syncVersion: false, + }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: Test\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/synced.yaml', specContent); + writeFile(dir, 'specs/unsynced.yaml', specContent); + writeFile(dir, '.contractual/snapshots/synced-api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/unsynced-api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'synced-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'unsynced-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump-both.md', + `---\n"synced-api": minor\n"unsynced-api": minor\n---\n\n## synced-api\n- Change\n\n## unsynced-api\n- Change\n` + ); + + run('version', dir); + + // Synced: spec version updated + const syncedSpec = readYAML(dir, 'specs/synced.yaml') as { info: { version: string } }; + expect(syncedSpec.info.version).toBe('1.1.0'); + + // Unsynced: spec version unchanged + const unsyncedSpec = readYAML(dir, 'specs/unsynced.yaml') as { + info: { version: string }; + }; + expect(unsyncedSpec.info.version).toBe('1.0.0'); + + // Both bumped in versions.json + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['synced-api'].version).toBe('1.1.0'); + expect(versions['unsynced-api'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + }); + + describe('--no-sync-version CLI flag', () => { + test('skips spec version update when --no-sync-version is passed', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": minor\n---\n\n## api\n- Feature\n` + ); + + run('version --no-sync-version', dir); + + // Spec should keep original version + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.0'); + + // But versions.json should still be bumped + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + + test('--no-sync-version overrides syncVersion: true in config', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml', syncVersion: true }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": minor\n---\n\n## api\n- Feature\n` + ); + + run('version --no-sync-version', dir); + + // CLI flag wins over config + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('missing version field in spec', () => { + test('creates info.version when missing from OpenAPI spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + // Write a spec WITHOUT info.version + writeFile( + dir, + 'specs/api.yaml', + `openapi: 3.0.3\ninfo:\n title: No Version API\npaths: {}\n` + ); + writeFile( + dir, + '.contractual/snapshots/api.yaml', + `openapi: 3.0.3\ninfo:\n title: No Version API\npaths: {}\n` + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '0.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/initial.md', + `---\n"api": minor\n---\n\n## api\n- Initial feature\n` + ); + + run('version', dir); + + // Version field should be created + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('0.1.0'); + } finally { + cleanup(); + } + }); + + test('creates info object and version when info is missing from OpenAPI spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + // Write a spec WITHOUT info at all + writeFile(dir, 'specs/api.yaml', `openapi: 3.0.3\npaths: {}\n`); + writeFile(dir, '.contractual/snapshots/api.yaml', `openapi: 3.0.3\npaths: {}\n`); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '0.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/initial.md', + `---\n"api": major\n---\n\n## api\n- Initial release\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info?: { version?: string } }; + expect(spec.info).toBeDefined(); + expect(spec.info!.version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + + test('creates info.version in JSON spec when missing', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.json' }, + ]); + + const specNoVersion = { openapi: '3.0.3', info: { title: 'Test' }, paths: {} }; + writeFile(dir, 'specs/api.json', JSON.stringify(specNoVersion, null, 2) + '\n'); + writeFile( + dir, + '.contractual/snapshots/api.json', + JSON.stringify(specNoVersion, null, 2) + '\n' + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": patch\n---\n\n## api\n- Fix\n` + ); + + run('version', dir); + + const spec = readJSON(dir, 'specs/api.json') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.1'); + } finally { + cleanup(); + } + }); + }); + + describe('all bump types', () => { + test('patch bump syncs spec version', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.2.3\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.2.3', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/fix.md', + `---\n"api": patch\n---\n\n## api\n- Bug fix\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.2.4'); + } finally { + cleanup(); + } + }); + + test('minor bump syncs spec version', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.2.3\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.2.3', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/feature.md', + `---\n"api": minor\n---\n\n## api\n- New feature\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.3.0'); + } finally { + cleanup(); + } + }); + + test('major bump syncs spec version', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 2.5.3\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '2.5.3', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/breaking.md', + `---\n"api": major\n---\n\n## api\n- Breaking change\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('3.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('first version (from 0.0.0)', () => { + test('syncs spec version on first bump from implicit 0.0.0', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + writeFile( + dir, + 'specs/api.yaml', + `openapi: 3.0.3\ninfo:\n title: New API\n version: 0.0.0\npaths: {}\n` + ); + writeFile( + dir, + '.contractual/snapshots/api.yaml', + `openapi: 3.0.3\ninfo:\n title: New API\n version: 0.0.0\npaths: {}\n` + ); + + // No initial version in versions.json (first release) + + writeFile( + dir, + '.contractual/changesets/initial.md', + `---\n"api": major\n---\n\n## api\n- Initial release\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.0'); + + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api'].version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('multiple changesets aggregation', () => { + test('spec gets the aggregated version (highest bump wins)', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' } }) + ); + + // Three changesets: patch, minor, major β†’ major wins β†’ 2.0.0 + writeFile( + dir, + '.contractual/changesets/fix.md', + `---\n"api": patch\n---\n\n## api\n- Fix\n` + ); + writeFile( + dir, + '.contractual/changesets/feature.md', + `---\n"api": minor\n---\n\n## api\n- Feature\n` + ); + writeFile( + dir, + '.contractual/changesets/breaking.md', + `---\n"api": major\n---\n\n## api\n- Breaking\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('2.0.0'); + } finally { + cleanup(); + } + }); + }); + + describe('multi-contract', () => { + test('syncs version in multiple OpenAPI specs from one changeset', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'orders-api', type: 'openapi', path: 'specs/orders.yaml' }, + { name: 'users-api', type: 'openapi', path: 'specs/users.yaml' }, + ]); + + const ordersSpec = `openapi: 3.0.3\ninfo:\n title: Orders\n version: 1.0.0\npaths: {}\n`; + const usersSpec = `openapi: 3.0.3\ninfo:\n title: Users\n version: 2.0.0\npaths: {}\n`; + writeFile(dir, 'specs/orders.yaml', ordersSpec); + writeFile(dir, 'specs/users.yaml', usersSpec); + writeFile(dir, '.contractual/snapshots/orders-api.yaml', ordersSpec); + writeFile(dir, '.contractual/snapshots/users-api.yaml', usersSpec); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'orders-api': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + 'users-api': { version: '2.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/update.md', + `---\n"orders-api": minor\n"users-api": patch\n---\n\n## orders-api\n- New endpoint\n\n## users-api\n- Fix typo\n` + ); + + run('version', dir); + + const ordersResult = readYAML(dir, 'specs/orders.yaml') as { info: { version: string } }; + expect(ordersResult.info.version).toBe('1.1.0'); + + const usersResult = readYAML(dir, 'specs/users.yaml') as { info: { version: string } }; + expect(usersResult.info.version).toBe('2.0.1'); + + // Snapshots too + const ordersSnapshot = readYAML(dir, '.contractual/snapshots/orders-api.yaml') as { + info: { version: string }; + }; + expect(ordersSnapshot.info.version).toBe('1.1.0'); + + const usersSnapshot = readYAML(dir, '.contractual/snapshots/users-api.yaml') as { + info: { version: string }; + }; + expect(usersSnapshot.info.version).toBe('2.0.1'); + } finally { + cleanup(); + } + }); + + test('mixed types: syncs OpenAPI, skips JSON Schema', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + { name: 'order', type: 'json-schema', path: 'schemas/order.json' }, + ]); + + const apiSpec = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', apiSpec); + writeFile(dir, '.contractual/snapshots/api.yaml', apiSpec); + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + copyFixture( + 'json-schema/order-base.json', + path.join(dir, '.contractual/snapshots/order.json') + ); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + order: { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + const schemaBefore = readFile(dir, 'schemas/order.json'); + + writeFile( + dir, + '.contractual/changesets/bump.md', + `---\n"api": minor\n"order": minor\n---\n\n## api\n- Feature\n\n## order\n- Field added\n` + ); + + run('version', dir); + + // OpenAPI spec updated + const apiResult = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(apiResult.info.version).toBe('1.1.0'); + + // JSON Schema unchanged + const schemaAfter = readFile(dir, 'schemas/order.json'); + expect(schemaAfter).toBe(schemaBefore); + + // Both bumped in versions.json + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api'].version).toBe('1.1.0'); + expect(versions['order'].version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + }); + + describe('pre-release version sync', () => { + test('syncs pre-release version into spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'api', type: 'openapi', path: 'specs/api.yaml' }, + ]); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 1.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + writeFile(dir, '.contractual/snapshots/api.yaml', specContent); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ api: { version: '1.0.0', released: '2026-01-01T00:00:00Z' } }) + ); + + // Enter pre-release mode + run('pre enter beta', dir); + + writeFile( + dir, + '.contractual/changesets/feature.md', + `---\n"api": minor\n---\n\n## api\n- Beta feature\n` + ); + + run('version --yes', dir); + + // Spec should have pre-release version + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toMatch(/^1\.1\.0-beta/); + + // versions.json should match + const versions = readJSON(dir, '.contractual/versions.json') as Record< + string, + { version: string } + >; + expect(versions['api'].version).toBe(spec.info.version); + } finally { + cleanup(); + } + }); + }); + + describe('AsyncAPI', () => { + test('updates info.version in AsyncAPI spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'events', type: 'asyncapi', path: 'specs/events.yaml' }, + ]); + + const asyncSpec = `asyncapi: 2.6.0\ninfo:\n title: Events API\n version: 1.0.0\nchannels: {}\n`; + writeFile(dir, 'specs/events.yaml', asyncSpec); + writeFile(dir, '.contractual/snapshots/events.yaml', asyncSpec); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ events: { version: '1.0.0', released: '2026-01-01T00:00:00Z' } }) + ); + + writeFile( + dir, + '.contractual/changesets/event-change.md', + `---\n"events": minor\n---\n\n## events\n- Added new channel\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/events.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.1.0'); + + // Snapshot too + const snapshot = readYAML(dir, '.contractual/snapshots/events.yaml') as { + info: { version: string }; + }; + expect(snapshot.info.version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + }); + + describe('ODCS', () => { + test('updates top-level version in ODCS spec', () => { + const { dir, cleanup } = createTempRepo(); + try { + setupRepoWithConfig(dir, [ + { name: 'data-contract', type: 'odcs', path: 'specs/contract.yaml' }, + ]); + + const odcsSpec = `dataContractSpecification: 3.0.0\nkind: DataContract\nversion: 1.0.0\ninfo:\n title: Sales Data\n`; + writeFile(dir, 'specs/contract.yaml', odcsSpec); + writeFile(dir, '.contractual/snapshots/data-contract.yaml', odcsSpec); + + writeFile( + dir, + '.contractual/versions.json', + JSON.stringify({ + 'data-contract': { version: '1.0.0', released: '2026-01-01T00:00:00Z' }, + }) + ); + + writeFile( + dir, + '.contractual/changesets/schema-update.md', + `---\n"data-contract": minor\n---\n\n## data-contract\n- Added new column\n` + ); + + run('version', dir); + + const spec = readYAML(dir, 'specs/contract.yaml') as { version: string }; + expect(spec.version).toBe('1.1.0'); + + // Snapshot too + const snapshot = readYAML(dir, '.contractual/snapshots/data-contract.yaml') as { + version: string; + }; + expect(snapshot.version).toBe('1.1.0'); + } finally { + cleanup(); + } + }); + }); + + describe('contract add', () => { + test('syncs initial version into spec on contract add', () => { + const { dir, cleanup } = createTempRepo(); + try { + // Create contractual.yaml with empty contracts + writeFile(dir, 'contractual.yaml', 'contracts: []\n'); + writeFile(dir, '.contractual/versions.json', '{}'); + + const specContent = `openapi: 3.0.3\ninfo:\n title: API\n version: 0.0.0\npaths: {}\n`; + writeFile(dir, 'specs/api.yaml', specContent); + + run( + 'contract add --name my-api --type openapi --path specs/api.yaml --initial-version 1.0.0 --yes --skip-validation', + dir + ); + + // Spec version should be set to 1.0.0 + const spec = readYAML(dir, 'specs/api.yaml') as { info: { version: string } }; + expect(spec.info.version).toBe('1.0.0'); + + // Snapshot should also have it + expect(fileExists(dir, '.contractual/snapshots/my-api.yaml')).toBe(true); + const snapshot = readYAML(dir, '.contractual/snapshots/my-api.yaml') as { + info: { version: string }; + }; + expect(snapshot.info.version).toBe('1.0.0'); + } finally { + cleanup(); + } + }); + }); +}); diff --git a/tests/e2e/22-advanced-security.test.ts b/tests/e2e/22-advanced-security.test.ts new file mode 100644 index 0000000..1c8b44d --- /dev/null +++ b/tests/e2e/22-advanced-security.test.ts @@ -0,0 +1,63 @@ +import { afterEach, describe, expect, test } from 'vitest'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { parse } from 'yaml'; +import { lintJsonSchema } from '../../packages/governance/linters/json-schema-ajv.js'; + +const directories: string[] = []; +afterEach(() => { + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +describe('schema draft URL recognition', () => { + test.each([ + ['http://json-schema.org/draft-04/schema#', false], + ['https://json-schema.org/draft-06/schema#', false], + ['https://json-schema.org/draft-07/schema#', false], + ['https://json-schema.org/draft/2019-09/schema', false], + ['https://json-schema.org/draft/2020-12/schema', false], + ['https://json-schema.org.evil.test/draft-07/schema', true], + ['https://evil-json-schema.org/draft-07/schema', true], + ['https://evil.test/json-schema.org/draft-07/schema', true], + ['https://evil.test/?draft-07=json-schema.org', true], + ['https://json-schema.org@evil.test/draft-07/schema', true], + ['https://json-schema.org/unknown/schema', true], + ['https://json-schema.org/draft-07/schema/extra', true], + ['ftp://json-schema.org/draft-07/schema', true], + ['not-a-url', true], + ])('recognizes only supported drafts on the official host: %s', async ($schema, unknown) => { + const directory = mkdtempSync(join(tmpdir(), 'contractual-schema-uri-')); + directories.push(directory); + const path = join(directory, 'schema.json'); + writeFileSync(path, JSON.stringify({ $schema, type: 'object' })); + const result = await lintJsonSchema(path, { skipMetaValidation: true, skipStyleRules: true }); + expect(result.warnings.some((issue) => issue.rule === 'unknown-draft')).toBe(unknown); + }); +}); + +describe('release-preview trust boundary', () => { + const workflow = parse( + readFileSync(new URL('../../.github/workflows/release-preview.yml', import.meta.url), 'utf8') + ); + + test('only executes trusted next code, without a caller-selected checkout', () => { + expect(workflow.on.workflow_dispatch.inputs).not.toHaveProperty('target_branch'); + expect(workflow.jobs.preview.if).toBe("github.ref == 'refs/heads/next'"); + const checkout = workflow.jobs.preview.steps.find((step: { uses?: string }) => + step.uses?.startsWith('actions/checkout@') + ); + expect(checkout.with.ref).toBe('next'); + expect(checkout.with['persist-credentials']).toBe(false); + }); + + test('keeps read-only permissions and does not restore or save caches', () => { + expect(workflow.permissions).toEqual({ contents: 'read' }); + for (const step of workflow.jobs.preview.steps) { + expect(step.uses || '').not.toMatch(/^actions\/cache(?:\/|@)/); + expect(step.with?.cache).toBeUndefined(); + } + }); +}); diff --git a/tests/e2e/22-release-safety.test.ts b/tests/e2e/22-release-safety.test.ts new file mode 100644 index 0000000..d6fb700 --- /dev/null +++ b/tests/e2e/22-release-safety.test.ts @@ -0,0 +1,41 @@ +import { test, expect } from 'vitest'; +import { createTempRepo, writeFile, run, fileExists, readFile } from './helpers.js'; + +test('fixed versioning fails before modifying versions or consuming changesets', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + 'contracts:\n - name: api\n type: json-schema\n path: api.json\nversioning:\n mode: fixed\n' + ); + writeFile(dir, 'api.json', '{"type":"object"}'); + writeFile(dir, '.contractual/versions.json', '{"api":{"version":"1.0.0"}}'); + writeFile(dir, '.contractual/changesets/change.md', '---\napi: minor\n---\nChange\n'); + const result = run('version --yes', dir, { expectFail: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('Fixed versioning is not implemented'); + expect(readFile(dir, '.contractual/versions.json')).toBe('{"api":{"version":"1.0.0"}}'); + expect(fileExists(dir, '.contractual/changesets/change.md')).toBe(true); + } finally { + cleanup(); + } +}); + +test('a missing built-in engine cannot report a successful lint or diff', () => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile( + dir, + 'contractual.yaml', + 'contracts:\n - name: events\n type: asyncapi\n path: events.yaml\n' + ); + writeFile(dir, 'events.yaml', 'asyncapi: 3.0.0\ninfo:\n title: Events\n version: 1.0.0\n'); + writeFile(dir, '.contractual/versions.json', '{"events":{"version":"1.0.0"}}'); + writeFile(dir, '.contractual/snapshots/events.yaml', 'asyncapi: 3.0.0\n'); + expect(run('lint --format json', dir, { expectFail: true }).exitCode).not.toBe(0); + expect(run('diff', dir, { expectFail: true }).exitCode).not.toBe(0); + } finally { + cleanup(); + } +}); diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts new file mode 100644 index 0000000..9ff5074 --- /dev/null +++ b/tests/e2e/helpers.ts @@ -0,0 +1,308 @@ +import { execSync } from 'node:child_process'; +import { + mkdtempSync, + cpSync, + readFileSync, + existsSync, + writeFileSync, + readdirSync, + rmSync, + mkdirSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +/** Path to the built CLI binary */ +const CLI_BIN = path.resolve(__dirname, '../../packages/cli/bin/cli.js'); + +/** Path to the fixtures directory */ +const FIXTURES_DIR = path.resolve(__dirname, '../fixtures'); + +/** + * Create a temp directory that auto-cleans after test + */ +export function createTempRepo(): { dir: string; cleanup: () => void } { + const dir = mkdtempSync(path.join(tmpdir(), 'contractual-e2e-')); + + return { + dir, + cleanup: () => { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }, + }; +} + +/** + * Create a temp directory WITH git initialized (for tests that need git commits) + */ +export function createTempGitRepo(): { dir: string; cleanup: () => void } { + const dir = mkdtempSync(path.join(tmpdir(), 'contractual-e2e-')); + + // Initialize git repo with explicit error handling + try { + execSync('git init --initial-branch=main', { cwd: dir, encoding: 'utf-8' }); + execSync('git config user.email "test@test.com"', { cwd: dir, encoding: 'utf-8' }); + execSync('git config user.name "Test User"', { cwd: dir, encoding: 'utf-8' }); + } catch (err) { + // Clean up on failure + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + const error = err as Error & { stderr?: string }; + throw new Error(`Failed to initialize git repo: ${error.message}\n${error.stderr ?? ''}`); + } + + // Verify .git directory exists + if (!existsSync(path.join(dir, '.git'))) { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + throw new Error(`Git init succeeded but .git directory not found in ${dir}`); + } + + return { + dir, + cleanup: () => { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }, + }; +} + +/** + * Run a contractual CLI command in the given directory + */ +export function run( + command: string, + cwd: string, + options?: { expectFail?: boolean; env?: Record; timeout?: number } +): { stdout: string; stderr: string; exitCode: number } { + const fullCmd = `node "${CLI_BIN}" ${command}`; + + try { + const stdout = execSync(fullCmd, { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: options?.timeout, + env: { ...process.env, ...options?.env, NO_COLOR: '1', FORCE_COLOR: '0' }, + }); + return { stdout, stderr: '', exitCode: 0 }; + } catch (err: unknown) { + const error = err as { stdout?: string; stderr?: string; status?: number }; + if (!options?.expectFail) { + // Unexpected failure β€” include output for debugging + throw new Error( + `Command failed: ${fullCmd}\n` + + `Exit code: ${error.status}\n` + + `stdout: ${error.stdout}\n` + + `stderr: ${error.stderr}` + ); + } + return { + stdout: error.stdout ?? '', + stderr: error.stderr ?? '', + exitCode: error.status ?? 1, + }; + } +} + +/** + * Copy fixture files into the temp repo + */ +export function copyFixtures(fixtureDir: string, destDir: string): void { + const srcDir = path.join(FIXTURES_DIR, fixtureDir); + cpSync(srcDir, destDir, { recursive: true }); +} + +/** + * Copy a single fixture file + */ +export function copyFixture(fixturePath: string, destPath: string): void { + const src = path.join(FIXTURES_DIR, fixturePath); + const destDir = path.dirname(destPath); + + // Ensure destination directory exists + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + + cpSync(src, destPath); +} + +/** + * Read a file from the temp repo + */ +export function readFile(repoDir: string, relativePath: string): string { + return readFileSync(path.join(repoDir, relativePath), 'utf-8'); +} + +/** + * Check if a file exists in the temp repo + */ +export function fileExists(repoDir: string, relativePath: string): boolean { + return existsSync(path.join(repoDir, relativePath)); +} + +/** + * Write a file in the temp repo + */ +export function writeFile(repoDir: string, relativePath: string, content: string): void { + const fullPath = path.join(repoDir, relativePath); + const dir = path.dirname(fullPath); + + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(fullPath, content); +} + +/** + * List files in a directory within the temp repo + */ +export function listFiles(repoDir: string, relativePath: string): string[] { + const fullPath = path.join(repoDir, relativePath); + if (!existsSync(fullPath)) return []; + return readdirSync(fullPath); +} + +/** + * Read and parse JSON from the temp repo + */ +export function readJSON(repoDir: string, relativePath: string): unknown { + return JSON.parse(readFile(repoDir, relativePath)); +} + +/** + * Read and parse YAML from the temp repo + */ +export function readYAML(repoDir: string, relativePath: string): unknown { + const content = readFile(repoDir, relativePath); + return parseYaml(content); +} + +/** + * Git commit all changes in the temp repo + */ +export function gitCommitAll(repoDir: string, message: string): void { + // Verify .git directory exists + if (!existsSync(path.join(repoDir, '.git'))) { + throw new Error(`gitCommitAll: Not a git repository: ${repoDir}`); + } + + try { + execSync('git add -A', { cwd: repoDir, encoding: 'utf-8' }); + execSync(`git commit -m "${message}" --allow-empty`, { cwd: repoDir, encoding: 'utf-8' }); + } catch (err) { + const error = err as Error & { stderr?: string; stdout?: string }; + throw new Error( + `gitCommitAll failed in ${repoDir}: ${error.message}\nstderr: ${error.stderr ?? ''}\nstdout: ${error.stdout ?? ''}` + ); + } +} + +/** + * Modify a YAML file (read, transform, write back) + */ +export function modifyYAML( + repoDir: string, + relativePath: string, + transform: (content: unknown) => unknown +): void { + const content = parseYaml(readFile(repoDir, relativePath)); + const modified = transform(content); + writeFile(repoDir, relativePath, stringifyYaml(modified as Record)); +} + +/** + * Modify a JSON file (read, transform, write back) + */ +export function modifyJSON( + repoDir: string, + relativePath: string, + transform: (content: unknown) => unknown +): void { + const content = readJSON(repoDir, relativePath); + const modified = transform(content); + writeFile(repoDir, relativePath, JSON.stringify(modified, null, 2) + '\n'); +} + +/** + * Setup a repo with a contractual.yaml config and .contractual directory + */ +export function setupRepoWithConfig( + repoDir: string, + contracts: Array<{ + name: string; + type: string; + path: string; + lint?: string | false; + breaking?: string | false; + syncVersion?: boolean; + }> +): void { + const config = { + contracts: contracts.map((c) => ({ + name: c.name, + type: c.type, + path: c.path, + ...(c.lint !== undefined ? { lint: c.lint } : {}), + ...(c.breaking !== undefined ? { breaking: c.breaking } : {}), + ...(c.syncVersion !== undefined ? { syncVersion: c.syncVersion } : {}), + })), + }; + + writeFile(repoDir, 'contractual.yaml', stringifyYaml(config)); + + // Create .contractual directory structure + mkdirSync(path.join(repoDir, '.contractual/changesets'), { recursive: true }); + mkdirSync(path.join(repoDir, '.contractual/snapshots'), { recursive: true }); + + if (!fileExists(repoDir, '.contractual/versions.json')) { + writeFile(repoDir, '.contractual/versions.json', '{}'); + } +} + +/** + * Check if oasdiff is available on the system + */ +export function checkOasdiff(): boolean { + try { + execSync('oasdiff --version', { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +/** + * Ensure CLI is built before running tests + */ +export function ensureCliBuilt(): void { + if (!existsSync(CLI_BIN)) { + throw new Error( + `CLI binary not found at ${CLI_BIN}. Run 'pnpm build' in the root directory first.` + ); + } + + // Also check that the dist directory has the commands.js file + const commandsPath = path.resolve(__dirname, '../../packages/cli/dist/commands.js'); + if (!existsSync(commandsPath)) { + throw new Error( + `CLI not built properly. dist/commands.js not found. Run 'pnpm build' in the root directory.` + ); + } +} diff --git a/tests/fixtures/json-schema/order-additional-props-denied.json b/tests/fixtures/json-schema/order-additional-props-denied.json new file mode 100644 index 0000000..a59cf5f --- /dev/null +++ b/tests/fixtures/json-schema/order-additional-props-denied.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": false + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-base.json b/tests/fixtures/json-schema/order-base.json new file mode 100644 index 0000000..cec8f1b --- /dev/null +++ b/tests/fixtures/json-schema/order-base.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-constraint-tightened.json b/tests/fixtures/json-schema/order-constraint-tightened.json new file mode 100644 index 0000000..4b94b01 --- /dev/null +++ b/tests/fixtures/json-schema/order-constraint-tightened.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 5 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 200 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-description-changed.json b/tests/fixtures/json-schema/order-description-changed.json new file mode 100644 index 0000000..86de9a2 --- /dev/null +++ b/tests/fixtures/json-schema/order-description-changed.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the e-commerce system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier (UUID format)" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status in the fulfillment pipeline" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-enum-value-added.json b/tests/fixtures/json-schema/order-enum-value-added.json new file mode 100644 index 0000000..99e03b4 --- /dev/null +++ b/tests/fixtures/json-schema/order-enum-value-added.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered", "cancelled"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-enum-value-removed.json b/tests/fixtures/json-schema/order-enum-value-removed.json new file mode 100644 index 0000000..7c836a9 --- /dev/null +++ b/tests/fixtures/json-schema/order-enum-value-removed.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-field-removed.json b/tests/fixtures/json-schema/order-field-removed.json new file mode 100644 index 0000000..4b8cb38 --- /dev/null +++ b/tests/fixtures/json-schema/order-field-removed.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-identical.json b/tests/fixtures/json-schema/order-identical.json new file mode 100644 index 0000000..cec8f1b --- /dev/null +++ b/tests/fixtures/json-schema/order-identical.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-optional-field-added.json b/tests/fixtures/json-schema/order-optional-field-added.json new file mode 100644 index 0000000..bb1166e --- /dev/null +++ b/tests/fixtures/json-schema/order-optional-field-added.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + }, + "tracking_number": { + "type": "string", + "description": "Shipment tracking number" + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-required-added.json b/tests/fixtures/json-schema/order-required-added.json new file mode 100644 index 0000000..249e0b3 --- /dev/null +++ b/tests/fixtures/json-schema/order-required-added.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "string", + "description": "Order total amount as string" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at", "customer_email"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema/order-type-changed.json b/tests/fixtures/json-schema/order-type-changed.json new file mode 100644 index 0000000..c9c585a --- /dev/null +++ b/tests/fixtures/json-schema/order-type-changed.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/order.schema.json", + "title": "Order", + "description": "An order in the system", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier" + }, + "status": { + "type": "string", + "enum": ["pending", "processing", "shipped", "delivered"], + "description": "Current order status" + }, + "amount": { + "type": "number", + "description": "Order total amount as number" + }, + "currency": { + "type": "string", + "format": "iso-4217", + "description": "Currency code (ISO 4217)" + }, + "items": { + "type": "array", + "description": "Line items in the order", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "price": { + "type": "string" + } + }, + "required": ["sku", "quantity", "price"] + } + }, + "shipping_address": { + "type": "object", + "description": "Shipping address", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string", + "maxLength": 10 + }, + "country": { + "type": "string" + } + }, + "required": ["street", "city", "zip", "country"] + }, + "customer_email": { + "type": "string", + "format": "email", + "description": "Customer email address" + }, + "notes": { + "type": "string", + "description": "Optional order notes", + "maxLength": 500 + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Order creation timestamp" + }, + "metadata": { + "type": "object", + "description": "Additional metadata", + "additionalProperties": true + } + }, + "required": ["id", "status", "amount", "currency", "items", "created_at"], + "additionalProperties": false +} diff --git a/tests/fixtures/openapi/circular-refs-base.yaml b/tests/fixtures/openapi/circular-refs-base.yaml new file mode 100644 index 0000000..ebfbcd7 --- /dev/null +++ b/tests/fixtures/openapi/circular-refs-base.yaml @@ -0,0 +1,60 @@ +openapi: 3.1.0 +info: + title: Circular Refs API + version: 1.0.0 +paths: + /categories: + get: + operationId: listCategories + summary: List categories (tree structure with circular refs) + responses: + "200": + description: A list of categories + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Category' + /nodes: + get: + operationId: listNodes + summary: List nodes (linked list with circular refs) + responses: + "200": + description: A list of nodes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Node' +components: + schemas: + Category: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + parent: + $ref: '#/components/schemas/Category' + children: + type: array + items: + $ref: '#/components/schemas/Category' + Node: + type: object + required: + - id + properties: + id: + type: integer + value: + type: string + next: + $ref: '#/components/schemas/Node' diff --git a/tests/fixtures/openapi/circular-refs-breaking.yaml b/tests/fixtures/openapi/circular-refs-breaking.yaml new file mode 100644 index 0000000..a0a0463 --- /dev/null +++ b/tests/fixtures/openapi/circular-refs-breaking.yaml @@ -0,0 +1,61 @@ +openapi: 3.1.0 +info: + title: Circular Refs API + version: 1.0.0 +paths: + /categories: + get: + operationId: listCategories + summary: List categories (tree structure with circular refs) + responses: + "200": + description: A list of categories + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Category' + /nodes: + get: + operationId: listNodes + summary: List nodes (linked list with circular refs) + responses: + "200": + description: A list of nodes + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Node' +components: + schemas: + Category: + type: object + required: + - id + - name + properties: + id: + # BREAKING: changed from integer to string + type: string + name: + type: string + parent: + $ref: '#/components/schemas/Category' + children: + type: array + items: + $ref: '#/components/schemas/Category' + Node: + type: object + required: + - id + properties: + id: + type: integer + value: + type: string + next: + $ref: '#/components/schemas/Node' diff --git a/tests/fixtures/openapi/petstore-31-base.yaml b/tests/fixtures/openapi/petstore-31-base.yaml new file mode 100644 index 0000000..0cace6b --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-base.yaml @@ -0,0 +1,103 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-breaking-endpoint-removed.yaml b/tests/fixtures/openapi/petstore-31-breaking-endpoint-removed.yaml new file mode 100644 index 0000000..14b3ea0 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-breaking-endpoint-removed.yaml @@ -0,0 +1,85 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + # BREAKING: /pets/{petId} path removed entirely +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-breaking-required-param.yaml b/tests/fixtures/openapi/petstore-31-breaking-required-param.yaml new file mode 100644 index 0000000..f0f6191 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-breaking-required-param.yaml @@ -0,0 +1,109 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + # BREAKING: new required query parameter + - name: apiKey + in: query + required: true + schema: + type: string + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-breaking-type-narrowed.yaml b/tests/fixtures/openapi/petstore-31-breaking-type-narrowed.yaml new file mode 100644 index 0000000..9b57ab5 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-breaking-type-narrowed.yaml @@ -0,0 +1,102 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + # BREAKING: narrowed from ["string", "null"] to just "string" β€” null no longer allowed + type: string + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-description-changed.yaml b/tests/fixtures/openapi/petstore-31-description-changed.yaml new file mode 100644 index 0000000..552b3b9 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-description-changed.yaml @@ -0,0 +1,107 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + # CHANGED: description updated + summary: An updated sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + # CHANGED: operation summary updated + summary: List all available pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + # CHANGED: operation description added + description: Creates a new pet in the store. Requires a valid pet name. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-nonbreaking-optional-param.yaml b/tests/fixtures/openapi/petstore-31-nonbreaking-optional-param.yaml new file mode 100644 index 0000000..14954d6 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-nonbreaking-optional-param.yaml @@ -0,0 +1,109 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + # NON-BREAKING: new optional query parameter + - name: offset + in: query + required: false + schema: + type: integer + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-31-nonbreaking-type-widened.yaml b/tests/fixtures/openapi/petstore-31-nonbreaking-type-widened.yaml new file mode 100644 index 0000000..b26efd3 --- /dev/null +++ b/tests/fixtures/openapi/petstore-31-nonbreaking-type-widened.yaml @@ -0,0 +1,106 @@ +openapi: 3.1.0 +info: + title: Petstore - OpenAPI 3.1 + summary: A sample petstore using OpenAPI 3.1 features + license: + name: MIT + identifier: MIT + version: 1.0.0 +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +servers: +- url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: List all pets + parameters: + - name: limit + in: query + required: false + schema: + type: integer + exclusiveMaximum: 100 + responses: + "200": + description: A list of pets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + post: + operationId: createPet + summary: Create a pet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewPet' + responses: + "201": + description: Pet created + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pets/{petId}: + get: + operationId: getPetById + summary: Get a pet by ID + parameters: + - name: petId + in: path + required: true + schema: + type: string + responses: + "200": + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "404": + description: Pet not found +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + # NON-BREAKING: widened from integer to [integer, string] + type: + - integer + - string + format: int64 + name: + type: string + tag: + type: + - string + - "null" + status: + type: string + enum: + - available + - pending + - sold + NewPet: + type: object + required: + - name + properties: + name: + type: string + tag: + type: + - string + - "null" diff --git a/tests/fixtures/openapi/petstore-base.yaml b/tests/fixtures/openapi/petstore-base.yaml new file mode 100644 index 0000000..de3d03c --- /dev/null +++ b/tests/fixtures/openapi/petstore-base.yaml @@ -0,0 +1,830 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/openapi/petstore-breaking-endpoint-removed.yaml b/tests/fixtures/openapi/petstore-breaking-endpoint-removed.yaml new file mode 100644 index 0000000..ae1b38e --- /dev/null +++ b/tests/fixtures/openapi/petstore-breaking-endpoint-removed.yaml @@ -0,0 +1,790 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/openapi/petstore-breaking-type-changed.yaml b/tests/fixtures/openapi/petstore-breaking-type-changed.yaml new file mode 100644 index 0000000..615ff40 --- /dev/null +++ b/tests/fixtures/openapi/petstore-breaking-type-changed.yaml @@ -0,0 +1,829 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: string + example: "10" + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/openapi/petstore-identical.yaml b/tests/fixtures/openapi/petstore-identical.yaml new file mode 100644 index 0000000..de3d03c --- /dev/null +++ b/tests/fixtures/openapi/petstore-identical.yaml @@ -0,0 +1,830 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/openapi/petstore-nonbreaking-description.yaml b/tests/fixtures/openapi/petstore-nonbreaking-description.yaml new file mode 100644 index 0000000..8901a5c --- /dev/null +++ b/tests/fixtures/openapi/petstore-nonbreaking-description.yaml @@ -0,0 +1,830 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). We've switched to the design first approach! + Help us improve the API by making changes to the definition itself or to the code. + Over time, we can improve the API and expose new features in OAS3. + + Useful links: + - [Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [Source API definition](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/openapi/petstore-nonbreaking-endpoint-added.yaml b/tests/fixtures/openapi/petstore-nonbreaking-endpoint-added.yaml new file mode 100644 index 0000000..46599cf --- /dev/null +++ b/tests/fixtures/openapi/petstore-nonbreaking-endpoint-added.yaml @@ -0,0 +1,856 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27 +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Pet deleted + "400": + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + "400": + description: No file uploaded + "404": + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/health: + get: + tags: + - pet + summary: Check pet service health. + description: Returns the health status of the pet service. + operationId: getPetHealth + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + timestamp: + type: string + format: date-time + default: + description: Unexpected error + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid input + "422": + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or non-integers will generate API errors. + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: order deleted + "400": + description: Invalid ID supplied + "404": + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + "200": + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + "200": + description: successful operation + "400": + description: bad request + "404": + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "200": + description: User deleted + "400": + description: Invalid username supplied + "404": + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/fixtures/tool-outputs/oasdiff/breaking-endpoint-removed.json b/tests/fixtures/tool-outputs/oasdiff/breaking-endpoint-removed.json new file mode 100644 index 0000000..42f0e6c --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/breaking-endpoint-removed.json @@ -0,0 +1 @@ +[{"id":"api-path-removed-without-deprecation","text":"api path removed without deprecation","level":3,"operation":"GET","operationId":"findPetsByTags","path":"/pet/findByTags","source":"openapi/petstore-base.yaml","section":"paths"}] diff --git a/tests/fixtures/tool-outputs/oasdiff/breaking-type-changed.json b/tests/fixtures/tool-outputs/oasdiff/breaking-type-changed.json new file mode 100644 index 0000000..94f6cf3 --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/breaking-type-changed.json @@ -0,0 +1 @@ +[{"id":"request-property-type-changed","text":"the 'id' request property type/format changed from 'integer'/'int64' to 'string'/'' (media type: application/json)","level":3,"operation":"POST","operationId":"placeOrder","path":"/store/order","source":"openapi/petstore-breaking-type-changed.yaml","section":"paths"},{"id":"response-property-type-changed","text":"the 'id' response's property type/format changed from 'integer'/'int64' to 'string'/'' for status '200'","level":3,"operation":"POST","operationId":"placeOrder","path":"/store/order","source":"openapi/petstore-breaking-type-changed.yaml","section":"paths"},{"id":"response-property-type-changed","text":"the 'id' response's property type/format changed from 'integer'/'int64' to 'string'/'' for status '200' (media type: application/xml)","level":3,"operation":"GET","operationId":"getOrderById","path":"/store/order/{orderId}","source":"openapi/petstore-breaking-type-changed.yaml","section":"paths"},{"id":"response-property-type-changed","text":"the 'id' response's property type/format changed from 'integer'/'int64' to 'string'/'' for status '200' (media type: application/json)","level":3,"operation":"GET","operationId":"getOrderById","path":"/store/order/{orderId}","source":"openapi/petstore-breaking-type-changed.yaml","section":"paths"}] diff --git a/tests/fixtures/tool-outputs/oasdiff/changelog-endpoint-removed.json b/tests/fixtures/tool-outputs/oasdiff/changelog-endpoint-removed.json new file mode 100644 index 0000000..42f0e6c --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/changelog-endpoint-removed.json @@ -0,0 +1 @@ +[{"id":"api-path-removed-without-deprecation","text":"api path removed without deprecation","level":3,"operation":"GET","operationId":"findPetsByTags","path":"/pet/findByTags","source":"openapi/petstore-base.yaml","section":"paths"}] diff --git a/tests/fixtures/tool-outputs/oasdiff/diff-endpoint-removed.json b/tests/fixtures/tool-outputs/oasdiff/diff-endpoint-removed.json new file mode 100644 index 0000000..59bdd3a --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/diff-endpoint-removed.json @@ -0,0 +1 @@ +{"paths":{"deleted":["/pet/findByTags"]}} diff --git a/tests/fixtures/tool-outputs/oasdiff/no-changes.json b/tests/fixtures/tool-outputs/oasdiff/no-changes.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/no-changes.json @@ -0,0 +1 @@ +[] diff --git a/tests/fixtures/tool-outputs/oasdiff/nonbreaking-endpoint-added.json b/tests/fixtures/tool-outputs/oasdiff/nonbreaking-endpoint-added.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/fixtures/tool-outputs/oasdiff/nonbreaking-endpoint-added.json @@ -0,0 +1 @@ +[] diff --git a/tsconfig.build.json b/tsconfig.build.json index 32971be..5ea453e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,10 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "incremental": false, "sourceMap": false }, "exclude": [ diff --git a/tsconfig.json b/tsconfig.json index 26aa473..33aeced 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { "compilerOptions": { - "module": "esnext", - "target": "esnext", - "skipLibCheck": true, + "module": "NodeNext", + "target": "ES2022", + "moduleResolution": "NodeNext", "declaration": true, + "declarationMap": true, + "skipLibCheck": true, "removeComments": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "baseUrl": "./", "outDir": "./dist", - "incremental": false, "noImplicitAny": true, "strictNullChecks": true, - "moduleResolution": "node", "esModuleInterop": true, "resolveJsonModule": true, "types": [ diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts new file mode 100644 index 0000000..166305d --- /dev/null +++ b/vitest.config.e2e.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/e2e/**/*.test.ts'], + testTimeout: 60_000, + hookTimeout: 30_000, + teardownTimeout: 30_000, + // Run serially β€” each test creates temp dirs, parallel risks port/path conflicts + fileParallelism: false, + globals: true, + server: { + deps: { + inline: ['yaml'], + }, + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 05725e6..5394774 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import process from 'process'; export default defineConfig({ test: { - workspace: ['packages/*', 'packages/generators/*', 'packages/providers/*'], + include: ['packages/**/*.test.ts', 'packages/**/*.spec.ts'], reporters: ['default', 'junit'], outputFile: { junit: