diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml new file mode 100644 index 0000000..bc9c82e --- /dev/null +++ b/.github/workflows/js.yml @@ -0,0 +1,311 @@ +name: JavaScript CI/CD + +on: + push: + branches: + - main + paths: + - 'js/**' + - '.github/workflows/js.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'js/**' + - '.github/workflows/js.yml' + workflow_dispatch: + inputs: + release_mode: + description: 'Manual release mode' + required: true + type: choice + default: 'instant' + options: + - instant + - changeset-pr + bump_type: + description: 'Manual release type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Manual release description (optional)' + required: false + type: string + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + # Changeset check - only runs on PRs + changeset-check: + name: Check for Changesets + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Check for changesets + working-directory: ./js + run: | + # Skip changeset check for automated version PRs + if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then + echo "Skipping changeset check for automated release PR" + exit 0 + fi + + # Run changeset validation script + node scripts/validate-changeset.mjs + + # Linting and formatting - runs after changeset check on PRs, immediately on main + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + needs: [changeset-check] + if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Run ESLint + working-directory: ./js + run: npm run lint + + - name: Check formatting + working-directory: ./js + run: npm run format:check + + - name: Check code duplication + working-directory: ./js + run: npm run check:duplication + + # Test matrix: Node.js on multiple OS + test: + name: Test (Node.js on ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: [changeset-check] + if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Run tests + working-directory: ./js + run: npm test + + - name: Run example + working-directory: ./js + run: npm run example + + # Release - only runs on main after tests pass (for push events) + release: + name: Release + needs: [lint, test] + # Use always() to ensure this job runs even if changeset-check was skipped + # This is needed because lint/test jobs have a transitive dependency on changeset-check + if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success' + runs-on: ubuntu-latest + # Permissions required for npm OIDC trusted publishing + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Update npm for OIDC trusted publishing + working-directory: ./js + run: node scripts/setup-npm.mjs + + - name: Check for changesets + id: check_changesets + working-directory: ./js + run: | + # Count changeset files (excluding README.md and config.json) + CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l) + echo "Found $CHANGESET_COUNT changeset file(s)" + echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT + + - name: Version packages and commit to main + if: steps.check_changesets.outputs.has_changesets == 'true' + id: version + working-directory: ./js + run: node scripts/version-and-commit.mjs --mode changeset + + - name: Publish to npm + # Run if version was committed OR if a previous attempt already committed (for re-runs) + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + id: publish + working-directory: ./js + run: node scripts/publish-to-npm.mjs --should-pull + + - name: Create GitHub Release + if: steps.publish.outputs.published == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./js + run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" + + - name: Format GitHub release notes + if: steps.publish.outputs.published == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./js + run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" + + # Manual Instant Release - triggered via workflow_dispatch with instant mode + instant-release: + name: Instant Release + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' + runs-on: ubuntu-latest + # Permissions required for npm OIDC trusted publishing + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Update npm for OIDC trusted publishing + working-directory: ./js + run: node scripts/setup-npm.mjs + + - name: Version packages and commit to main + id: version + working-directory: ./js + run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Publish to npm + # Run if version was committed OR if a previous attempt already committed (for re-runs) + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + id: publish + working-directory: ./js + run: node scripts/publish-to-npm.mjs + + - name: Create GitHub Release + if: steps.publish.outputs.published == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./js + run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" + + - name: Format GitHub release notes + if: steps.publish.outputs.published == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./js + run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" + + # Manual Changeset PR - creates a pull request with the changeset for review + changeset-pr: + name: Create Changeset PR + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./js + run: npm install + + - name: Create changeset file + working-directory: ./js + run: node scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Format changeset with Prettier + working-directory: ./js + run: | + # Run Prettier on the changeset file to ensure it matches project style + npx prettier --write ".changeset/*.md" || true + + echo "Formatted changeset files" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release' + branch: changeset-manual-release-${{ github.run_id }} + delete-branch: true + title: 'chore: manual ${{ github.event.inputs.bump_type }} release (JS)' + body: | + ## Manual Release Request (JavaScript) + + This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release for the JavaScript package. + + ### Release Details + - **Type:** ${{ github.event.inputs.bump_type }} + - **Description:** ${{ github.event.inputs.description || 'Manual release' }} + - **Triggered by:** @${{ github.actor }} + + ### Next Steps + 1. Review the changeset in this PR + 2. Merge this PR to main + 3. The automated release workflow will create a version PR + 4. Merge the version PR to publish to npm and create a GitHub release diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..9556ce1 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,322 @@ +name: Python CI/CD + +on: + push: + branches: + - main + paths: + - 'python/**' + - '.github/workflows/python.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'python/**' + - '.github/workflows/python.yml' + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Release description (optional)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # REQUIRED CI CHECKS - All must pass before release + # These jobs ensure code quality and tests pass before any release + + # Linting and formatting + lint: + name: Lint and Format Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + working-directory: ./python + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run Ruff linting + working-directory: ./python + run: ruff check src tests + + - name: Check Ruff formatting + working-directory: ./python + run: ruff format --check src tests + + - name: Run mypy + working-directory: ./python + run: mypy src + + - name: Check file size limit + working-directory: ./python + run: python scripts/check_file_size.py + + # Test on latest Python version only + test: + name: Test (Python 3.13) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + working-directory: ./python + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + working-directory: ./python + run: pytest tests/ -v --cov=src --cov-report=xml --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./python/coverage.xml + fail_ci_if_error: false + + # Build package - only runs if lint and test pass + build: + name: Build Package + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + working-directory: ./python + run: python -m build + + - name: Check package + working-directory: ./python + run: twine check dist/* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: python/dist/ + + # Check for changelog fragments in PRs (similar to changesets check) + changelog: + name: Changelog Fragment Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install scriv + run: pip install "scriv[toml]" + + - name: Check for changelog fragments + working-directory: ./python + run: | + # Get list of fragment files (excluding README and template) + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" ! -name "*.j2" 2>/dev/null | wc -l) + + # Get changed files in PR + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + # Check if any source files changed (excluding docs and config) + SOURCE_CHANGED=$(echo "$CHANGED_FILES" | grep -E "^python/(src/|tests/|scripts/)" | wc -l) + + if [ "$SOURCE_CHANGED" -gt 0 ] && [ "$FRAGMENTS" -eq 0 ]; then + echo "::warning::No changelog fragment found. Please run 'scriv create' and document your changes." + echo "" + echo "To create a changelog fragment:" + echo " cd python" + echo " pip install 'scriv[toml]'" + echo " scriv create" + echo "" + echo "This is similar to adding a changeset in JavaScript projects." + echo "See python/changelog.d/README.md for more information." + # Note: This is a warning, not a failure, to allow flexibility + # Change 'exit 0' to 'exit 1' to make it required + exit 0 + fi + + echo "✓ Changelog check passed" + + # RELEASE JOBS - Only run after all CI checks pass + + # Automatic release on push to main (if version changed) + auto-release: + name: Auto Release + needs: [lint, test, build] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Check if version changed + id: version_check + working-directory: ./python + run: | + # Get current version from pyproject.toml + CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' pyproject.toml) + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + # Check if tag exists + if git rev-parse "python-v$CURRENT_VERSION" >/dev/null 2>&1; then + echo "Tag python-v$CURRENT_VERSION already exists, skipping release" + echo "should_release=false" >> $GITHUB_OUTPUT + else + echo "New version detected: $CURRENT_VERSION" + echo "should_release=true" >> $GITHUB_OUTPUT + fi + + - name: Download artifacts + if: steps.version_check.outputs.should_release == 'true' + uses: actions/download-artifact@v4 + with: + name: dist + path: python/dist/ + + - name: Publish to PyPI + if: steps.version_check.outputs.should_release == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: python/dist/ + + - name: Create GitHub Release + if: steps.version_check.outputs.should_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./python + run: | + python scripts/create_github_release.py \ + --version "${{ steps.version_check.outputs.current_version }}" \ + --repository "${{ github.repository }}" \ + --tag-prefix "python-v" + + # Manual release via workflow_dispatch - only after CI passes + manual-release: + name: Manual Release + needs: [lint, test, build] + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + working-directory: ./python + run: | + python -m pip install --upgrade pip + pip install build twine "scriv[toml]" + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Collect changelog fragments + working-directory: ./python + run: | + # Check if there are any fragments to collect + FRAGMENTS=$(find changelog.d -name "*.md" ! -name "README.md" ! -name "*.j2" 2>/dev/null | wc -l) + if [ "$FRAGMENTS" -gt 0 ]; then + echo "Found $FRAGMENTS changelog fragment(s), collecting..." + scriv collect --version "${{ github.event.inputs.bump_type }}" + else + echo "No changelog fragments found, skipping collection" + fi + + - name: Version and commit + id: version + working-directory: ./python + run: | + python scripts/version_and_commit.py \ + --bump-type "${{ github.event.inputs.bump_type }}" \ + --description "${{ github.event.inputs.description }}" + + - name: Build package + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + working-directory: ./python + run: python -m build + + - name: Check package + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + working-directory: ./python + run: twine check dist/* + + - name: Publish to PyPI + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: python/dist/ + + - name: Create GitHub Release + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: ./python + run: | + python scripts/create_github_release.py \ + --version "${{ steps.version.outputs.new_version }}" \ + --repository "${{ github.repository }}" \ + --tag-prefix "python-v" diff --git a/js/.changeset/README.md b/js/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/js/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/js/.changeset/ci-cd-integration.md b/js/.changeset/ci-cd-integration.md new file mode 100644 index 0000000..5501493 --- /dev/null +++ b/js/.changeset/ci-cd-integration.md @@ -0,0 +1,5 @@ +--- +'lino-objects-codec': patch +--- + +Add complete CI/CD workflows and release automation for both JavaScript and Python packages diff --git a/js/.changeset/config.json b/js/.changeset/config.json new file mode 100644 index 0000000..2be13d4 --- /dev/null +++ b/js/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/js/.jscpd.json b/js/.jscpd.json new file mode 100644 index 0000000..9869261 --- /dev/null +++ b/js/.jscpd.json @@ -0,0 +1,20 @@ +{ + "threshold": 0, + "minTokens": 30, + "minLines": 5, + "skipComments": true, + "ignore": [ + "**/node_modules/**", + "**/build/**", + "**/dist/**", + "**/*.min.js", + "**/coverage/**", + "**/.changeset/**", + "**/package-lock.json", + "**/pnpm-lock.yaml", + "**/yarn.lock" + ], + "format": "console", + "reporters": ["console", "html"], + "output": "./reports/jscpd" +} diff --git a/js/.prettierignore b/js/.prettierignore new file mode 100644 index 0000000..6fd6704 --- /dev/null +++ b/js/.prettierignore @@ -0,0 +1,7 @@ +node_modules +coverage +dist +*.min.js +package-lock.json +.eslintcache +CLAUDE.md diff --git a/js/.prettierrc b/js/.prettierrc new file mode 100644 index 0000000..3171de5 --- /dev/null +++ b/js/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/js/README.md b/js/README.md index 908c013..428c9e0 100644 --- a/js/README.md +++ b/js/README.md @@ -1,11 +1,13 @@ # lino-objects-codec (JavaScript) A JavaScript library for working with Links Notation format. This library provides: + - Universal serialization/deserialization for JavaScript objects with circular reference support - JSON to Links Notation conversion utilities - Fuzzy matching utilities for string comparison These tools enable easy implementation of higher-level features like: + - [LinksNotationManager](https://github.com/konard/follow/blob/main/lino.lib.mjs) - Intermediate application data storage - [Q&A Database](https://github.com/konard/hh-job-application-automation/blob/main/src/qa-database.mjs) - Questions and answers database @@ -248,6 +250,7 @@ The library uses the [links-notation](https://github.com/link-foundation/links-n - Circular references use special `ref` links: `(ref obj_0)` This approach allows for: + - Universal representation of object graphs - Preservation of object identity - Natural handling of circular references @@ -262,12 +265,15 @@ This approach allows for: Encode a JavaScript object to Links Notation format with type markers. **Parameters:** + - `options.obj` - The JavaScript object to encode **Returns:** + - String representation in Links Notation format **Throws:** + - `TypeError` - If the object type is not supported #### `decode({ notation: notation })` @@ -275,9 +281,11 @@ Encode a JavaScript object to Links Notation format with type markers. Decode Links Notation format to a JavaScript object. **Parameters:** + - `options.notation` - String in Links Notation format **Returns:** + - Reconstructed JavaScript object #### `ObjectCodec` @@ -299,9 +307,11 @@ const decoded = codec.decode({ notation: encoded }); Convert JSON data to Links Notation format. **Parameters:** + - `options.json` - Any JSON-serializable value (object, array, string, number, boolean, null) **Returns:** + - Links Notation string representation ```javascript @@ -317,9 +327,11 @@ jsonToLino({ json: [1, 2, 3] }); Convert Links Notation to JSON. **Parameters:** + - `options.lino` - Links Notation string **Returns:** + - Parsed JSON value ```javascript @@ -332,9 +344,11 @@ linoToJson({ lino: '((name Alice }) (age 30))'); Escape a value for safe use in Links Notation format. **Parameters:** + - `options.value` - The value to escape (string, number, or boolean) **Returns:** + - Escaped string suitable for Links Notation ```javascript @@ -348,9 +362,11 @@ escapeReference({ value: "it's" }); // "\"it's\"" Unescape a Links Notation reference. **Parameters:** + - `options.str` - The escaped reference string **Returns:** + - Unescaped string #### `formatAsLino(options = {})` @@ -358,9 +374,11 @@ Unescape a Links Notation reference. Format an array as Links Notation with proper indentation. **Parameters:** + - `options.values` - Array of values **Returns:** + - Formatted Links Notation string ### Fuzzy Matching Utilities @@ -370,9 +388,11 @@ Format an array as Links Notation with proper indentation. Calculate edit distance between two strings. **Parameters:** + - `options.a`, `options.b` - Strings to compare **Returns:** + - Number of edits (insertions, deletions, substitutions) needed #### `stringSimilarity(options = {})` @@ -380,9 +400,11 @@ Calculate edit distance between two strings. Calculate normalized similarity score between two strings. **Parameters:** + - `options.a`, `options.b` - Strings to compare **Returns:** + - Score between 0 (completely different) and 1 (identical) #### `normalizeQuestion({ question: question })` @@ -390,9 +412,11 @@ Calculate normalized similarity score between two strings. Normalize a question for comparison (lowercase, remove punctuation, standardize whitespace). **Parameters:** + - `options.question` - Question string **Returns:** + - Normalized string #### `extractKeywords(options = {})` @@ -400,12 +424,14 @@ Normalize a question for comparison (lowercase, remove punctuation, standardize Extract meaningful keywords from a question, optionally filtering out stopwords. **Parameters:** + - `options.question` - Question string - `options.stopwords` - Custom stopwords set to filter out (default: empty Set, no filtering) - `options.minWordLength` - Minimum word length (default: 2) - `options.stemLength` - Length for word stemming (default: 5, 0 to disable) **Returns:** + - Set of keywords #### `keywordSimilarity(options = {})` @@ -413,10 +439,12 @@ Extract meaningful keywords from a question, optionally filtering out stopwords. Calculate keyword overlap similarity (Jaccard index). **Parameters:** + - `options.a`, `options.b` - Questions to compare - `options` - Same as extractKeywords **Returns:** + - Score between 0 and 1 #### `findBestMatch({ question: question, qaDatabase: database, options })` @@ -424,6 +452,7 @@ Calculate keyword overlap similarity (Jaccard index). Find the best matching question from a database. **Parameters:** + - `options.question` - Question to match - `options.qaDatabase` - Map of questions to answers - `options.threshold` - Minimum similarity threshold (default: 0.4) @@ -434,6 +463,7 @@ Find the best matching question from a database. - `options.stemLength` - Stem length for keyword extraction **Returns:** + - `{ question, answer, score }` or null if no match above threshold #### `findAllMatches({ question: question, qaDatabase: database, options })` @@ -441,9 +471,11 @@ Find the best matching question from a database. Find all matches above a threshold, sorted by score. **Parameters:** + - Same as findBestMatch **Returns:** + - Array of `{ question, answer, score }` sorted by score descending ## Development diff --git a/js/eslint.config.js b/js/eslint.config.js new file mode 100644 index 0000000..a394d0d --- /dev/null +++ b/js/eslint.config.js @@ -0,0 +1,91 @@ +import js from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; +import prettierPlugin from 'eslint-plugin-prettier'; + +export default [ + js.configs.recommended, + prettierConfig, + { + files: ['**/*.js', '**/*.mjs'], + plugins: { + prettier: prettierPlugin, + }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + // Node.js globals + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + // Node.js 18+ globals + fetch: 'readonly', + // Runtime-specific globals + Bun: 'readonly', + Deno: 'readonly', + }, + }, + rules: { + // Prettier integration + 'prettier/prettier': 'error', + + // Code quality rules + 'no-unused-vars': 'error', + 'no-console': 'off', // Allow console in this project + 'no-debugger': 'error', + + // Best practices + eqeqeq: ['error', 'always'], + curly: ['error', 'all'], + 'no-var': 'error', + 'prefer-const': 'error', + 'prefer-arrow-callback': 'error', + 'no-duplicate-imports': 'error', + + // ES6+ features + 'arrow-body-style': ['error', 'as-needed'], + 'object-shorthand': ['error', 'always'], + 'prefer-template': 'error', + + // Async/await + 'no-async-promise-executor': 'error', + 'require-await': 'warn', + + // Comments and documentation + 'spaced-comment': ['error', 'always', { markers: ['/'] }], + + // Complexity rules - reasonable thresholds for maintainability + complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8 + 'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4 + 'max-lines-per-function': [ + 'warn', + { + max: 150, // More reasonable than strict 50 lines per function + skipBlankLines: true, + skipComments: true, + }, + ], + 'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5 + 'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions + 'max-lines': ['error', 1500], // Maximum lines per file - counts all lines including blank lines and comments + }, + }, + { + // Test files have different requirements + files: ['tests/**/*.js', '**/*.test.js'], + rules: { + 'require-await': 'off', // Async functions without await are common in tests + }, + }, + { + ignores: [ + 'node_modules/**', + 'coverage/**', + 'dist/**', + '*.min.js', + '.eslintcache', + ], + }, +]; diff --git a/js/examples/basic_usage.js b/js/examples/basic_usage.js index 2cf0a18..a64ecc2 100644 --- a/js/examples/basic_usage.js +++ b/js/examples/basic_usage.js @@ -24,7 +24,9 @@ function main() { const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); const encodedPreview = encoded.substring(0, 50); - console.log(` ${String(obj).padEnd(30)} -> ${encodedPreview.padEnd(50)} -> ${decoded}`); + console.log( + ` ${String(obj).padEnd(30)} -> ${encodedPreview.padEnd(50)} -> ${decoded}` + ); // Handle NaN case (NaN !== NaN) const isEqual = (obj !== obj && decoded !== decoded) || decoded === obj; if (!isEqual) { @@ -42,14 +44,18 @@ function main() { console.log(` Encoded: ${encodedArray}`); const decodedArray = decode({ notation: encodedArray }); console.log(` Decoded: ${JSON.stringify(decodedArray)}`); - console.log(` Match: ${JSON.stringify(decodedArray) === JSON.stringify(arrayExample)}`); + console.log( + ` Match: ${JSON.stringify(decodedArray) === JSON.stringify(arrayExample)}` + ); console.log(`\n Object: ${JSON.stringify(objectExample)}`); const encodedObject = encode({ obj: objectExample }); console.log(` Encoded: ${encodedObject}`); const decodedObject = decode({ notation: encodedObject }); console.log(` Decoded: ${JSON.stringify(decodedObject)}`); - console.log(` Match: ${JSON.stringify(decodedObject) === JSON.stringify(objectExample)}`); + console.log( + ` Match: ${JSON.stringify(decodedObject) === JSON.stringify(objectExample)}` + ); // Example 3: Nested structures console.log('\n3. Nested Structures:'); @@ -65,7 +71,9 @@ function main() { console.log(` Encoded length: ${encodedNested.length} characters`); const decodedNested = decode({ notation: encodedNested }); console.log(` Decoded: ${JSON.stringify(decodedNested)}`); - console.log(` Match: ${JSON.stringify(decodedNested) === JSON.stringify(nested)}`); + console.log( + ` Match: ${JSON.stringify(decodedNested) === JSON.stringify(nested)}` + ); // Example 4: Circular references console.log('\n4. Circular References:'); @@ -77,8 +85,12 @@ function main() { const encodedCircular = encode({ obj: arr }); console.log(` Encoded: ${encodedCircular}`); const decodedCircular = decode({ notation: encodedCircular }); - console.log(` Decoded correctly: ${JSON.stringify(decodedCircular.slice(0, 3)) === '[1,2,3]'}`); - console.log(` Circular reference preserved: ${decodedCircular[3] === decodedCircular}`); + console.log( + ` Decoded correctly: ${JSON.stringify(decodedCircular.slice(0, 3)) === '[1,2,3]'}` + ); + console.log( + ` Circular reference preserved: ${decodedCircular[3] === decodedCircular}` + ); if (decodedCircular[3] !== decodedCircular) { console.error(' ERROR: Circular reference not preserved!'); } @@ -91,7 +103,9 @@ function main() { console.log(` Encoded: ${encodedObjectCircular}`); const decodedObjectCircular = decode({ notation: encodedObjectCircular }); console.log(` Decoded correctly: ${decodedObjectCircular.name === 'root'}`); - console.log(` Circular reference preserved: ${decodedObjectCircular.self === decodedObjectCircular}`); + console.log( + ` Circular reference preserved: ${decodedObjectCircular.self === decodedObjectCircular}` + ); if (decodedObjectCircular.self !== decodedObjectCircular) { console.error(' ERROR: Circular reference not preserved!'); } @@ -104,8 +118,9 @@ function main() { const encodedShared = encode({ obj: container }); console.log(` Encoded: ${encodedShared}`); const decodedShared = decode({ notation: encodedShared }); - const allSame = decodedShared.first === decodedShared.second && - decodedShared.second === decodedShared.third; + const allSame = + decodedShared.first === decodedShared.second && + decodedShared.second === decodedShared.third; console.log(` All three references point to same object: ${allSame}`); if (!allSame) { console.error(' ERROR: Shared references not preserved!'); @@ -113,9 +128,13 @@ function main() { // Modify through one reference decodedShared.first.modified = true; - console.log(` Modified through 'first', visible in 'second': ${decodedShared.second.modified === true}`); + console.log( + ` Modified through 'first', visible in 'second': ${decodedShared.second.modified === true}` + ); if (decodedShared.second.modified !== true) { - console.error(' ERROR: Modification not visible through shared reference!'); + console.error( + ' ERROR: Modification not visible through shared reference!' + ); } console.log('\n=== All examples completed successfully! ==='); diff --git a/js/package-lock.json b/js/package-lock.json index 5a50d73..2ad607b 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -11,15 +11,4072 @@ "dependencies": { "links-notation": "^0.11.0" }, + "devDependencies": { + "@changesets/cli": "^2.29.7", + "eslint": "^9.38.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jscpd": "^4.0.5", + "lint-staged": "^16.2.6", + "prettier": "^3.6.2" + }, "engines": { "node": ">=18.0.0" } }, - "node_modules/links-notation": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/links-notation/-/links-notation-0.11.2.tgz", - "integrity": "sha512-VPyELWBXpaCCiNPVeZhMbG7RuvOQR51nhqELK+s/rbSzKYhSs+tyiSOdQ7z8I7Kh3PLABF3bZETtWSFwx3vFfg==", - "license": "Unlicense" + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@changesets/apply-release-plan": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz", + "integrity": "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^3.1.2", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz", + "integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/cli": { + "version": "2.29.8", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz", + "integrity": "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^7.0.14", + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.2", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-release-plan": "^4.0.14", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.6", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "ci-info": "^3.7.0", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "p-limit": "^2.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" + }, + "bin": { + "changeset": "bin.js" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz", + "integrity": "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/logger": "^0.1.1", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz", + "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz", + "integrity": "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/config": "^3.1.2", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.6", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz", + "integrity": "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz", + "integrity": "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.2", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@changesets/write/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jscpd/core": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.1.tgz", + "integrity": "sha512-6Migc68Z8p7q5xqW1wbF3SfIbYHPQoiLHPbJb1A1Z1H9DwImwopFkYflqRDpuamLd0Jfg2jx3ZBmHQt21NbD1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/@jscpd/finder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.1.tgz", + "integrity": "sha512-TcCT28686GeLl87EUmrBXYmuOFELVMDwyjKkcId+qjNS1zVWRd53Xd5xKwEDzkCEgen/vCs+lorLLToolXp5oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.0.1", + "@jscpd/tokenizer": "4.0.1", + "blamer": "^1.0.6", + "bytes": "^3.1.2", + "cli-table3": "^0.6.5", + "colors": "^1.4.0", + "fast-glob": "^3.3.2", + "fs-extra": "^11.2.0", + "markdown-table": "^2.0.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/finder/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@jscpd/finder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@jscpd/finder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@jscpd/html-reporter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.1.tgz", + "integrity": "sha512-M9fFETNvXXuy4fWv0M2oMluxwrQUBtubxCHaWw21lb2G8A6SE19moe3dUkluZ/3V4BccywfeF9lSEUg84heLww==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "1.4.0", + "fs-extra": "^11.2.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/html-reporter/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@jscpd/html-reporter/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@jscpd/html-reporter/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@jscpd/tokenizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.1.tgz", + "integrity": "sha512-l/CPeEigadYcQUsUxf1wdCBfNjyAxYcQU04KciFNmSZAMY+ykJ8fZsiuyfjb+oOuDgsIPZZ9YvbvsCr6NBXueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.0.1", + "reprism": "^0.0.11", + "spark-md5": "^3.0.2" + } + }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/blamer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/blamer/-/blamer-1.0.7.tgz", + "integrity": "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^4.0.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gitignore-to-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/gitignore-to-glob/-/gitignore-to-glob-0.3.0.tgz", + "integrity": "sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.4 <5 || >=6.9" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jscpd": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.5.tgz", + "integrity": "sha512-AzJlSLvKtXYkQm93DKE1cRN3rf6pkpv3fm5TVuvECwoqljQlCM/56ujHn9xPcE7wyUnH5+yHr7tcTiveIoMBoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.0.1", + "@jscpd/finder": "4.0.1", + "@jscpd/html-reporter": "4.0.1", + "@jscpd/tokenizer": "4.0.1", + "colors": "^1.4.0", + "commander": "^5.0.0", + "fs-extra": "^11.2.0", + "gitignore-to-glob": "^0.3.0", + "jscpd-sarif-reporter": "4.0.3" + }, + "bin": { + "jscpd": "bin/jscpd" + } + }, + "node_modules/jscpd-sarif-reporter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.3.tgz", + "integrity": "sha512-0T7KiWiDIVArvlBkvCorn2NFwQe7p7DJ37o4YFRuPLDpcr1jNHQlEfbFPw8hDdgJ4hpfby6A5YwyHqASKJ7drA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "^1.4.0", + "fs-extra": "^11.2.0", + "node-sarif-builder": "^2.0.3" + } + }, + "node_modules/jscpd-sarif-reporter/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/jscpd-sarif-reporter/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jscpd-sarif-reporter/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jscpd/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/jscpd/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jscpd/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/links-notation": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/links-notation/-/links-notation-0.11.2.tgz", + "integrity": "sha512-VPyELWBXpaCCiNPVeZhMbG7RuvOQR51nhqELK+s/rbSzKYhSs+tyiSOdQ7z8I7Kh3PLABF3bZETtWSFwx3vFfg==", + "license": "Unlicense" + }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-sarif-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz", + "integrity": "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.4", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/node-sarif-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/node-sarif-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/node-sarif-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/pug": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/reprism": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/reprism/-/reprism-0.0.11.tgz", + "integrity": "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, + "node_modules/spawndamnit/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/js/package.json b/js/package.json index a28b71e..ed0ef3b 100644 --- a/js/package.json +++ b/js/package.json @@ -9,7 +9,18 @@ }, "scripts": { "test": "node --test tests/*.test.js", - "example": "node examples/basic_usage.js" + "example": "node examples/basic_usage.js", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check:duplication": "jscpd .", + "check": "npm run lint && npm run format:check && npm run check:duplication", + "prepare": "husky || true", + "changeset": "changeset", + "changeset:version": "node scripts/changeset-version.mjs", + "changeset:publish": "changeset publish", + "changeset:status": "changeset status --since=origin/main" }, "keywords": [ "links-notation", @@ -29,5 +40,26 @@ }, "engines": { "node": ">=18.0.0" + }, + "devDependencies": { + "@changesets/cli": "^2.29.7", + "eslint": "^9.38.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jscpd": "^4.0.5", + "lint-staged": "^16.2.6", + "prettier": "^3.6.2" + }, + "lint-staged": { + "*.{js,mjs,cjs}": [ + "eslint --fix --max-warnings 0", + "prettier --write", + "prettier --check" + ], + "*.md": [ + "prettier --write", + "prettier --check" + ] } } diff --git a/js/scripts/changeset-version.mjs b/js/scripts/changeset-version.mjs new file mode 100644 index 0000000..856b31f --- /dev/null +++ b/js/scripts/changeset-version.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +/** + * Custom changeset version script that ensures package-lock.json is synchronized + * with package.json after version bumps. + * + * This script: + * 1. Runs `changeset version` to update package versions + * 2. Runs `npm install` to synchronize package-lock.json with the new versions + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + */ + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import command-stream for shell command execution +const { $ } = await use('command-stream'); + +try { + console.log('Running changeset version...'); + await $`npx changeset version`; + + console.log('\nSynchronizing package-lock.json...'); + await $`npm install --package-lock-only`; + + console.log('\n✅ Version bump complete with synchronized package-lock.json'); +} catch (error) { + console.error('Error during version bump:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); + } + process.exit(1); +} diff --git a/js/scripts/create-github-release.mjs b/js/scripts/create-github-release.mjs new file mode 100644 index 0000000..e32dce3 --- /dev/null +++ b/js/scripts/create-github-release.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +/** + * Create GitHub Release from CHANGELOG.md + * Usage: node scripts/create-github-release.mjs --release-version --repository + * release-version: Version number (e.g., 1.0.0) + * repository: GitHub repository (e.g., owner/repo) + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('release-version', { + type: 'string', + default: getenv('VERSION', ''), + describe: 'Version number (e.g., 1.0.0)', + }) + .option('repository', { + type: 'string', + default: getenv('REPOSITORY', ''), + describe: 'GitHub repository (e.g., owner/repo)', + }), +}); + +const { releaseVersion: version, repository } = config; + +if (!version || !repository) { + console.error('Error: Missing required arguments'); + console.error( + 'Usage: node scripts/create-github-release.mjs --release-version --repository ' + ); + process.exit(1); +} + +const tag = `v${version}`; + +console.log(`Creating GitHub release for ${tag}...`); + +try { + // Read CHANGELOG.md + const changelog = readFileSync('./CHANGELOG.md', 'utf8'); + + // Extract changelog entry for this version + // Read from CHANGELOG.md between this version header and the next version header + const versionHeaderRegex = new RegExp(`## ${version}[\\s\\S]*?(?=## \\d|$)`); + const match = changelog.match(versionHeaderRegex); + + let releaseNotes = ''; + if (match) { + // Remove the version header itself and trim + releaseNotes = match[0].replace(`## ${version}`, '').trim(); + } + + if (!releaseNotes) { + releaseNotes = `Release ${version}`; + } + + // Create release using GitHub API with JSON input + // This avoids shell escaping issues that occur when passing text via command-line arguments + // (Previously caused apostrophes like "didn't" to appear as "didn'''" in releases) + const payload = JSON.stringify({ + tag_name: tag, + name: version, + body: releaseNotes, + }); + + await $`gh api repos/${repository}/releases -X POST --input -`.run({ + stdin: payload, + }); + + console.log(`\u2705 Created GitHub release: ${tag}`); +} catch (error) { + console.error('Error creating release:', error.message); + process.exit(1); +} diff --git a/js/scripts/create-manual-changeset.mjs b/js/scripts/create-manual-changeset.mjs new file mode 100644 index 0000000..37dfe15 --- /dev/null +++ b/js/scripts/create-manual-changeset.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +/** + * Create a changeset file for manual releases + * Usage: node scripts/create-manual-changeset.mjs --bump-type [--description ] + * + * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { writeFileSync } from 'fs'; +import { randomBytes } from 'crypto'; + +// TODO: Update this to match your package name in package.json +const PACKAGE_NAME = 'my-package'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('description', { + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for the changeset', + }), +}); + +try { + const { bumpType, description: descriptionArg } = config; + + // Use provided description or default based on bump type + const description = descriptionArg || `Manual ${bumpType} release`; + + if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/create-manual-changeset.mjs --bump-type [--description ]' + ); + process.exit(1); + } + + // Generate a random changeset ID + const changesetId = randomBytes(4).toString('hex'); + const changesetFile = `.changeset/manual-release-${changesetId}.md`; + + // Create the changeset file with single quotes to match Prettier config + const content = `--- +'${PACKAGE_NAME}': ${bumpType} +--- + +${description} +`; + + writeFileSync(changesetFile, content, 'utf-8'); + + console.log(`Created changeset: ${changesetFile}`); + console.log('Content:'); + console.log(content); + + // Format with Prettier + console.log('\nFormatting with Prettier...'); + await $`npx prettier --write "${changesetFile}"`; + + console.log('\n✅ Changeset created and formatted successfully'); +} catch (error) { + console.error('Error creating changeset:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); + } + process.exit(1); +} diff --git a/js/scripts/format-github-release.mjs b/js/scripts/format-github-release.mjs new file mode 100644 index 0000000..d40a20c --- /dev/null +++ b/js/scripts/format-github-release.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +/** + * Format GitHub release notes using the format-release-notes.mjs script + * Usage: node scripts/format-github-release.mjs --release-version --repository --commit-sha + * release-version: Version number (e.g., 1.0.0) + * repository: GitHub repository (e.g., owner/repo) + * commit_sha: Commit SHA for PR detection + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('release-version', { + type: 'string', + default: getenv('VERSION', ''), + describe: 'Version number (e.g., 1.0.0)', + }) + .option('repository', { + type: 'string', + default: getenv('REPOSITORY', ''), + describe: 'GitHub repository (e.g., owner/repo)', + }) + .option('commit-sha', { + type: 'string', + default: getenv('COMMIT_SHA', ''), + describe: 'Commit SHA for PR detection', + }), +}); + +const { releaseVersion: version, repository, commitSha } = config; + +if (!version || !repository || !commitSha) { + console.error('Error: Missing required arguments'); + console.error( + 'Usage: node scripts/format-github-release.mjs --release-version --repository --commit-sha ' + ); + process.exit(1); +} + +const tag = `v${version}`; + +try { + // Get the release ID for this version + let releaseId = ''; + try { + const result = + await $`gh api "repos/${repository}/releases/tags/${tag}" --jq '.id'`.run( + { capture: true } + ); + releaseId = result.stdout.trim(); + } catch { + console.log(`\u26A0\uFE0F Could not find release for ${tag}`); + process.exit(0); + } + + if (releaseId) { + console.log(`Formatting release notes for ${tag}...`); + // Pass the trigger commit SHA for PR detection + // This allows proper PR lookup even if the changelog doesn't have a commit hash + await $`node scripts/format-release-notes.mjs --release-id "${releaseId}" --release-version "${tag}" --repository "${repository}" --commit-sha "${commitSha}"`; + console.log(`\u2705 Formatted release notes for ${tag}`); + } +} catch (error) { + console.error('Error formatting release:', error.message); + process.exit(1); +} diff --git a/js/scripts/format-release-notes.mjs b/js/scripts/format-release-notes.mjs new file mode 100644 index 0000000..c6b6aaa --- /dev/null +++ b/js/scripts/format-release-notes.mjs @@ -0,0 +1,219 @@ +#!/usr/bin/env node + +/** + * Script to format GitHub release notes with proper formatting: + * - Fix special characters like \n + * - Add link to PR that contains the release commit (if found) + * - Add shields.io NPM version badge + * - Format nicely with proper markdown + * + * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json + * + * PR Detection Logic: + * 1. Extract commit hash from changelog entry (if present) + * 2. Fall back to --commit-sha argument (passed from workflow) + * 3. Look up PRs that contain the commit via GitHub API + * 4. If no PR found, simply don't display any PR link (no guessing) + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + * + * Note: Uses --release-version instead of --version to avoid conflict with yargs' built-in --version flag. + */ + +// TODO: Update this to match your package name in package.json +const PACKAGE_NAME = 'my-package'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('release-version', { + type: 'string', + default: getenv('VERSION', ''), + describe: 'Version number (e.g., v0.8.36)', + }) + .option('release-id', { + type: 'string', + default: getenv('RELEASE_ID', ''), + describe: 'GitHub release ID', + }) + .option('repository', { + type: 'string', + default: getenv('REPOSITORY', ''), + describe: 'GitHub repository (e.g., owner/repo)', + }) + .option('commit-sha', { + type: 'string', + default: getenv('COMMIT_SHA', ''), + describe: 'Commit SHA for PR detection', + }), +}); + +const releaseId = config.releaseId; +const version = config.releaseVersion; +const repository = config.repository; +const passedCommitSha = config.commitSha; + +if (!releaseId || !version || !repository) { + console.error( + 'Usage: format-release-notes.mjs --release-id --release-version --repository [--commit-sha ]' + ); + process.exit(1); +} + +try { + // Get current release body + const result = await $`gh api repos/${repository}/releases/${releaseId}`.run({ + capture: true, + }); + const releaseData = JSON.parse(result.stdout); + + const currentBody = releaseData.body || ''; + + // Skip if already formatted (has shields.io badge image) + if (currentBody.includes('img.shields.io')) { + console.log('ℹ️ Release notes already formatted'); + process.exit(0); + } + + // Extract changes section (Major, Minor, or Patch) + // This regex handles multiple formats: + // 1. With commit hash: "### [Major|Minor|Patch] Changes\n- abc1234: Description" + // 2. Without commit hash: "### [Major|Minor|Patch] Changes\n- Description" + const changesPattern = + /### (Major|Minor|Patch) Changes\s*\n\s*-\s+(?:([a-f0-9]+):\s+)?(.+?)$/s; + const changesMatch = currentBody.match(changesPattern); + + let commitHash = null; + let rawDescription = null; + let changeType = null; + + if (changesMatch) { + // Extract: [full match, changeType, commitHash (optional), description] + [, changeType, commitHash, rawDescription] = changesMatch; + console.log(`ℹ️ Found ${changeType} Changes section`); + + // If commitHash is undefined and description contains it, try to extract + if (!commitHash && rawDescription) { + // This handles the case where description itself might be null/undefined + // and we need to safely check for commit hash at the start + const descWithHashMatch = rawDescription.match(/^([a-f0-9]+):\s+(.+)$/s); + if (descWithHashMatch) { + [, commitHash, rawDescription] = descWithHashMatch; + } + } + } else { + console.log('⚠️ Could not parse changes from release notes'); + console.log(' Looking for pattern: ### [Major|Minor|Patch] Changes'); + process.exit(0); + } + + // Clean up the description: + // 1. Convert literal \n sequences (escaped newlines from GitHub API) to actual newlines + // 2. Remove leading/trailing quotes (including escaped quotes from command-stream shell escaping) + // 3. Remove any trailing npm package links or markdown that might be there + // 4. Normalize whitespace while preserving line breaks + const cleanDescription = rawDescription + .replace(/\\n/g, '\n') // Convert escaped \n to actual newlines + .replace(/^(\\['"])+/g, '') // Remove leading escaped quotes (e.g., \', \", \'', \'') + .replace(/(['"])+$/g, '') // Remove trailing unescaped quotes (e.g., ', ", '', '') + .replace(/^(['"])+/g, '') // Remove leading unescaped quotes + .replace(/📦.*$/s, '') // Remove any existing npm package info + .replace(/---.*$/s, '') // Remove any existing separators and everything after + .trim() + .split('\n') // Split by lines + .map((line) => line.trim()) // Trim whitespace from each line + .join('\n') // Rejoin with newlines + .replace(/\n{3,}/g, '\n\n'); // Normalize excessive blank lines (3+ becomes 2) + + // Find the PR that contains the release commit + // Uses commit hash from changelog or passed commit SHA from workflow + let prNumber = null; + + // Determine which commit SHA to use for PR lookup + const commitShaToLookup = commitHash || passedCommitSha; + + if (commitShaToLookup) { + const source = commitHash ? 'changelog' : 'workflow'; + console.log( + `ℹ️ Looking up PR for commit ${commitShaToLookup} (from ${source})` + ); + + try { + const prResult = + await $`gh api "repos/${repository}/commits/${commitShaToLookup}/pulls"`.run( + { capture: true } + ); + const prsData = JSON.parse(prResult.stdout); + + // Find the PR that's not the version bump PR (not "chore: version packages") + const relevantPr = prsData.find( + (pr) => !pr.title.includes('version packages') + ); + + if (relevantPr) { + prNumber = relevantPr.number; + console.log(`✅ Found PR #${prNumber} containing commit`); + } else if (prsData.length > 0) { + console.log( + '⚠️ Found PRs but all are version bump PRs, not linking any' + ); + } else { + console.log( + 'ℹ️ No PR found containing this commit - not adding PR link' + ); + } + } catch (error) { + console.log('⚠️ Could not find PR for commit', commitShaToLookup); + console.log(' Error:', error.message); + if (process.env.DEBUG) { + console.error(error); + } + } + } else { + // No commit hash available from any source + console.log('ℹ️ No commit SHA available - not adding PR link'); + } + + // Build formatted release notes + const versionWithoutV = version.replace(/^v/, ''); + const npmBadge = `[![npm version](https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`; + + let formattedBody = `${cleanDescription}`; + + // Add PR link if available + if (prNumber) { + formattedBody += `\n\n**Related Pull Request:** #${prNumber}`; + } + + formattedBody += `\n\n---\n\n${npmBadge}`; + + // Update the release using JSON input to properly handle special characters + const updatePayload = JSON.stringify({ body: formattedBody }); + await $`gh api repos/${repository}/releases/${releaseId} -X PATCH --input -`.run( + { stdin: updatePayload } + ); + + console.log(`✅ Formatted release notes for v${versionWithoutV}`); + if (prNumber) { + console.log(` - Added link to PR #${prNumber}`); + } + console.log(' - Added shields.io npm badge'); + console.log(' - Cleaned up formatting'); +} catch (error) { + console.error('❌ Error formatting release notes:', error.message); + process.exit(1); +} diff --git a/js/scripts/instant-version-bump.mjs b/js/scripts/instant-version-bump.mjs new file mode 100644 index 0000000..38da492 --- /dev/null +++ b/js/scripts/instant-version-bump.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node + +/** + * Instant version bump script for manual releases + * Bypasses the changeset workflow and directly updates version and changelog + * + * Usage: node scripts/instant-version-bump.mjs --bump-type [--description ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, writeFileSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type: major, minor, or patch', + choices: ['major', 'minor', 'patch'], + }) + .option('description', { + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for the version bump', + }), +}); + +try { + const { bumpType, description } = config; + const finalDescription = description || `Manual ${bumpType} release`; + + if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( + 'Usage: node scripts/instant-version-bump.mjs --bump-type [--description ]' + ); + process.exit(1); + } + + console.log(`\nBumping version (${bumpType})...`); + + // Get current version + const packageJson = JSON.parse(readFileSync('package.json', 'utf-8')); + const oldVersion = packageJson.version; + console.log(`Current version: ${oldVersion}`); + + // Bump version using npm version (doesn't create git tag) + await $`npm version ${bumpType} --no-git-tag-version`; + + // Get new version + const updatedPackageJson = JSON.parse(readFileSync('package.json', 'utf-8')); + const newVersion = updatedPackageJson.version; + console.log(`New version: ${newVersion}`); + + // Update CHANGELOG.md + console.log('\nUpdating CHANGELOG.md...'); + const changelogPath = 'CHANGELOG.md'; + let changelog = readFileSync(changelogPath, 'utf-8'); + + // Create new changelog entry + const newEntry = `## ${newVersion} + +### ${bumpType.charAt(0).toUpperCase() + bumpType.slice(1)} Changes + +- ${finalDescription} + +`; + + // Insert new entry after the first heading (# Changelog or similar) + // Look for the first ## heading and insert before it + const firstVersionMatch = changelog.match(/^## /m); + + if (firstVersionMatch) { + const insertPosition = firstVersionMatch.index; + changelog = + changelog.slice(0, insertPosition) + + newEntry + + changelog.slice(insertPosition); + } else { + // If no version headings exist, append after the main heading + const mainHeadingMatch = changelog.match(/^# .+$/m); + if (mainHeadingMatch) { + const insertPosition = + mainHeadingMatch.index + mainHeadingMatch[0].length; + changelog = `${changelog.slice(0, insertPosition)}\n\n${newEntry}${changelog.slice(insertPosition)}`; + } else { + // If no headings at all, prepend + changelog = `${newEntry}\n${changelog}`; + } + } + + writeFileSync(changelogPath, changelog, 'utf-8'); + console.log('✅ CHANGELOG.md updated'); + + // Synchronize package-lock.json + console.log('\nSynchronizing package-lock.json...'); + await $`npm install --package-lock-only`; + + console.log('\n✅ Instant version bump complete'); + console.log(`Version: ${oldVersion} → ${newVersion}`); +} catch (error) { + console.error('Error during instant version bump:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); + } + process.exit(1); +} diff --git a/js/scripts/publish-to-npm.mjs b/js/scripts/publish-to-npm.mjs new file mode 100644 index 0000000..a7c664e --- /dev/null +++ b/js/scripts/publish-to-npm.mjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node + +/** + * Publish to npm using OIDC trusted publishing + * Usage: node scripts/publish-to-npm.mjs [--should-pull] + * should_pull: Optional flag to pull latest changes before publishing (for release job) + * + * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, appendFileSync } from 'fs'; + +// TODO: Update this to match your package name in package.json +const PACKAGE_NAME = 'my-package'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs.option('should-pull', { + type: 'boolean', + default: getenv('SHOULD_PULL', false), + describe: 'Pull latest changes before publishing', + }), +}); + +const { shouldPull } = config; +const MAX_RETRIES = 3; +const RETRY_DELAY = 10000; // 10 seconds + +/** + * Sleep for specified milliseconds + * @param {number} ms + */ +function sleep(ms) { + return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } +} + +async function main() { + try { + if (shouldPull) { + // Pull the latest changes we just pushed + await $`git pull origin main`; + } + + // Get current version + const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); + const currentVersion = packageJson.version; + console.log(`Current version to publish: ${currentVersion}`); + + // Check if this version is already published on npm + console.log( + `Checking if version ${currentVersion} is already published...` + ); + const checkResult = + await $`npm view "${PACKAGE_NAME}@${currentVersion}" version`.run({ + capture: true, + }); + + // command-stream returns { code: 0 } on success, { code: 1 } on failure (e.g., E404) + // Exit code 0 means version exists, non-zero means version not found + if (checkResult.code === 0) { + console.log(`Version ${currentVersion} is already published to npm`); + setOutput('published', 'true'); + setOutput('published_version', currentVersion); + setOutput('already_published', 'true'); + return; + } else { + // Version not found on npm (E404), proceed with publish + console.log( + `Version ${currentVersion} not found on npm, proceeding with publish...` + ); + } + + // Publish to npm using OIDC trusted publishing with retry logic + for (let i = 1; i <= MAX_RETRIES; i++) { + console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`); + try { + await $`npm run changeset:publish`; + setOutput('published', 'true'); + setOutput('published_version', currentVersion); + console.log( + `\u2705 Published ${PACKAGE_NAME}@${currentVersion} to npm` + ); + return; + } catch (error) { + if (i < MAX_RETRIES) { + console.log( + `Publish failed: ${error.message}, waiting ${RETRY_DELAY / 1000}s before retry...` + ); + await sleep(RETRY_DELAY); + } + } + } + + console.error(`\u274C Failed to publish after ${MAX_RETRIES} attempts`); + process.exit(1); + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/js/scripts/setup-npm.mjs b/js/scripts/setup-npm.mjs new file mode 100644 index 0000000..03e010a --- /dev/null +++ b/js/scripts/setup-npm.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +/** + * Update npm for OIDC trusted publishing + * npm trusted publishing requires npm >= 11.5.1 + * Node.js 20.x ships with npm 10.x, so we need to update + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + */ + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import command-stream for shell command execution +const { $ } = await use('command-stream'); + +try { + // Get current npm version + const currentResult = await $`npm --version`.run({ capture: true }); + const currentVersion = currentResult.stdout.trim(); + console.log(`Current npm version: ${currentVersion}`); + + // Update npm to latest + await $`npm install -g npm@latest`; + + // Get updated npm version + const updatedResult = await $`npm --version`.run({ capture: true }); + const updatedVersion = updatedResult.stdout.trim(); + console.log(`Updated npm version: ${updatedVersion}`); +} catch (error) { + console.error('Error updating npm:', error.message); + process.exit(1); +} diff --git a/js/scripts/validate-changeset.mjs b/js/scripts/validate-changeset.mjs new file mode 100644 index 0000000..fdcda65 --- /dev/null +++ b/js/scripts/validate-changeset.mjs @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Validate changeset for CI - ensures exactly one valid changeset exists + * + * IMPORTANT: Update the package name below to match your package.json + */ + +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +// TODO: Update this to match your package name in package.json +const PACKAGE_NAME = 'lino-objects-codec'; + +try { + // Count changeset files (excluding README.md and config.json) + const changesetDir = '.changeset'; + const changesetFiles = readdirSync(changesetDir).filter( + (file) => file.endsWith('.md') && file !== 'README.md' + ); + + const changesetCount = changesetFiles.length; + console.log(`Found ${changesetCount} changeset file(s)`); + + // Ensure exactly one changeset file exists + if (changesetCount === 0) { + console.error( + "::error::No changeset found. Please add a changeset by running 'npm run changeset' and commit the result." + ); + process.exit(1); + } else if (changesetCount > 1) { + console.error( + `::error::Multiple changesets found (${changesetCount}). Each PR should have exactly ONE changeset.` + ); + console.error('::error::Found changeset files:'); + changesetFiles.forEach((file) => console.error(` ${file}`)); + process.exit(1); + } + + // Get the changeset file + const changesetFile = join(changesetDir, changesetFiles[0]); + console.log(`Validating changeset: ${changesetFile}`); + + // Read the changeset file + const content = readFileSync(changesetFile, 'utf-8'); + + // Check if changeset has a valid type (major, minor, or patch) + const versionTypeRegex = new RegExp( + `^['"]${PACKAGE_NAME}['"]:\\s+(major|minor|patch)`, + 'm' + ); + if (!versionTypeRegex.test(content)) { + console.error( + '::error::Changeset must specify a version type: major, minor, or patch' + ); + console.error(`::error::Expected format in ${changesetFile}:`); + console.error('::error::---'); + console.error(`::error::'${PACKAGE_NAME}': patch`); + console.error('::error::---'); + console.error('::error::'); + console.error('::error::Your description here'); + console.error('\nFile content:'); + console.error(content); + process.exit(1); + } + + // Extract description (everything after the closing ---) and check it's not empty + const parts = content.split('---'); + if (parts.length < 3) { + console.error( + '::error::Changeset must include a description of the changes' + ); + console.error( + "::error::The description should appear after the closing '---' in the changeset file" + ); + console.error(`::error::Current content of ${changesetFile}:`); + console.error(content); + process.exit(1); + } + + const description = parts.slice(2).join('---').trim(); + if (!description) { + console.error( + '::error::Changeset must include a description of the changes' + ); + console.error( + "::error::The description should appear after the closing '---' in the changeset file" + ); + console.error(`::error::Current content of ${changesetFile}:`); + console.error(content); + process.exit(1); + } + + // Extract version type + const versionTypeMatch = content.match(versionTypeRegex); + const versionType = versionTypeMatch ? versionTypeMatch[1] : 'unknown'; + + console.log('✅ Changeset validation passed'); + console.log(` Type: ${versionType}`); + console.log(` Description: ${description}`); +} catch (error) { + console.error('Error during changeset validation:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); + } + process.exit(1); +} diff --git a/js/scripts/version-and-commit.mjs b/js/scripts/version-and-commit.mjs new file mode 100644 index 0000000..2e3ad54 --- /dev/null +++ b/js/scripts/version-and-commit.mjs @@ -0,0 +1,237 @@ +#!/usr/bin/env node + +/** + * Version packages and commit to main + * Usage: node scripts/version-and-commit.mjs --mode [--bump-type ] [--description ] + * changeset: Run changeset version + * instant: Run instant version bump with bump_type (patch|minor|major) and optional description + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +import { readFileSync, appendFileSync, readdirSync } from 'fs'; + +// Load use-m dynamically +const { use } = eval( + await (await fetch('https://unpkg.com/use-m/use.js')).text() +); + +// Import link-foundation libraries +const { $ } = await use('command-stream'); +const { makeConfig } = await use('lino-arguments'); + +// Parse CLI arguments using lino-arguments +const config = makeConfig({ + yargs: ({ yargs, getenv }) => + yargs + .option('mode', { + type: 'string', + default: getenv('MODE', 'changeset'), + describe: 'Version mode: changeset or instant', + choices: ['changeset', 'instant'], + }) + .option('bump-type', { + type: 'string', + default: getenv('BUMP_TYPE', ''), + describe: 'Version bump type for instant mode: major, minor, or patch', + }) + .option('description', { + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for instant version bump', + }), +}); + +const { mode, bumpType, description } = config; + +// Debug: Log parsed configuration +console.log('Parsed configuration:', { + mode, + bumpType, + description: description || '(none)', +}); + +// Detect if positional arguments were used (common mistake) +const args = process.argv.slice(2); +if (args.length > 0 && !args[0].startsWith('--')) { + console.error('Error: Positional arguments detected!'); + console.error('Command line arguments:', args); + console.error(''); + console.error( + 'This script requires named arguments (--mode, --bump-type, --description).' + ); + console.error('Usage:'); + console.error(' Changeset mode:'); + console.error(' node scripts/version-and-commit.mjs --mode changeset'); + console.error(' Instant mode:'); + console.error( + ' node scripts/version-and-commit.mjs --mode instant --bump-type [--description ]' + ); + console.error(''); + console.error('Examples:'); + console.error( + ' node scripts/version-and-commit.mjs --mode instant --bump-type patch --description "Fix bug"' + ); + console.error(' node scripts/version-and-commit.mjs --mode changeset'); + process.exit(1); +} + +// Validation: Ensure mode is set correctly +if (mode !== 'changeset' && mode !== 'instant') { + console.error(`Invalid mode: "${mode}". Expected "changeset" or "instant".`); + console.error('Command line arguments:', process.argv.slice(2)); + process.exit(1); +} + +// Validation: Ensure bump type is provided for instant mode +if (mode === 'instant' && !bumpType) { + console.error('Error: --bump-type is required for instant mode'); + console.error( + 'Usage: node scripts/version-and-commit.mjs --mode instant --bump-type [--description ]' + ); + process.exit(1); +} + +/** + * Append to GitHub Actions output file + * @param {string} key + * @param {string} value + */ +function setOutput(key, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + appendFileSync(outputFile, `${key}=${value}\n`); + } +} + +/** + * Count changeset files (excluding README.md) + */ +function countChangesets() { + try { + const changesetDir = '.changeset'; + const files = readdirSync(changesetDir); + return files.filter((f) => f.endsWith('.md') && f !== 'README.md').length; + } catch { + return 0; + } +} + +/** + * Get package version + * @param {string} source - 'local' or 'remote' + */ +async function getVersion(source = 'local') { + if (source === 'remote') { + const result = await $`git show origin/main:package.json`.run({ + capture: true, + }); + return JSON.parse(result.stdout).version; + } + return JSON.parse(readFileSync('./package.json', 'utf8')).version; +} + +async function main() { + try { + // Configure git + await $`git config user.name "github-actions[bot]"`; + await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; + + // Check if remote main has advanced (handles re-runs after partial success) + console.log('Checking for remote changes...'); + await $`git fetch origin main`; + + const localHeadResult = await $`git rev-parse HEAD`.run({ capture: true }); + const localHead = localHeadResult.stdout.trim(); + + const remoteHeadResult = await $`git rev-parse origin/main`.run({ + capture: true, + }); + const remoteHead = remoteHeadResult.stdout.trim(); + + if (localHead !== remoteHead) { + console.log( + `Remote main has advanced (local: ${localHead}, remote: ${remoteHead})` + ); + console.log('This may indicate a previous attempt partially succeeded.'); + + // Check if the remote version is already the expected bump + const remoteVersion = await getVersion('remote'); + console.log(`Remote version: ${remoteVersion}`); + + // Check if there are changesets to process + const changesetCount = countChangesets(); + + if (changesetCount === 0) { + console.log('No changesets to process and remote has advanced.'); + console.log( + 'Assuming version bump was already completed in a previous attempt.' + ); + setOutput('version_committed', 'false'); + setOutput('already_released', 'true'); + setOutput('new_version', remoteVersion); + return; + } else { + console.log('Rebasing on remote main to incorporate changes...'); + await $`git rebase origin/main`; + } + } + + // Get current version before bump + const oldVersion = await getVersion(); + console.log(`Current version: ${oldVersion}`); + + if (mode === 'instant') { + console.log('Running instant version bump...'); + // Run instant version bump script + // Rely on command-stream's auto-quoting for proper argument handling + if (description) { + await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description}`; + } else { + await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType}`; + } + } else { + console.log('Running changeset version...'); + // Run changeset version to bump versions and update CHANGELOG + await $`npm run changeset:version`; + } + + // Get new version after bump + const newVersion = await getVersion(); + console.log(`New version: ${newVersion}`); + setOutput('new_version', newVersion); + + // Check if there are changes to commit + const statusResult = await $`git status --porcelain`.run({ capture: true }); + const status = statusResult.stdout.trim(); + + if (status) { + console.log('Changes detected, committing...'); + + // Stage all changes (package.json, package-lock.json, CHANGELOG.md, deleted changesets) + await $`git add -A`; + + // Commit with version number as message + const commitMessage = newVersion; + const escapedMessage = commitMessage.replace(/"/g, '\\"'); + await $`git commit -m "${escapedMessage}"`; + + // Push directly to main + await $`git push origin main`; + + console.log('\u2705 Version bump committed and pushed to main'); + setOutput('version_committed', 'true'); + } else { + console.log('No changes to commit'); + setOutput('version_committed', 'false'); + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/js/src/codec.js b/js/src/codec.js index 7b4a943..7a7d80e 100644 --- a/js/src/codec.js +++ b/js/src/codec.js @@ -36,7 +36,7 @@ export class ObjectCodec { */ _makeLink(...parts) { // Each part becomes a Link with that id - const values = parts.map(part => new Link(part)); + const values = parts.map((part) => new Link(part)); return new Link(undefined, values); } @@ -125,8 +125,13 @@ export class ObjectCodec { // Handle case where format() creates output like (obj_0) which parser wraps // The parser returns a wrapper Link with no ID, containing the actual Link as first value - if (!link.id && link.values && link.values.length === 1 && - link.values[0].id && link.values[0].id.startsWith('obj_')) { + if ( + !link.id && + link.values && + link.values.length === 1 && + link.values[0].id && + link.values[0].id.startsWith('obj_') + ) { // Extract the actual Link link = link.values[0]; } @@ -143,7 +148,7 @@ export class ObjectCodec { _encodeValue(obj, visited = new Set()) { // Check if we've seen this object before (for circular references and shared objects) // Only track objects and arrays (mutable types) - if (obj !== null && (typeof obj === 'object')) { + if (obj !== null && typeof obj === 'object') { if (this._encodeMemo.has(obj)) { // Return a direct reference using the object's ID const refId = this._encodeMemo.get(obj); @@ -226,7 +231,10 @@ export class ObjectCodec { return new Link(refId, [new Link(ObjectCodec.TYPE_ARRAY), ...parts]); } else { // Wrap in a type marker for arrays without IDs: (array item1 item2 ...) - return new Link(undefined, [new Link(ObjectCodec.TYPE_ARRAY), ...parts]); + return new Link(undefined, [ + new Link(ObjectCodec.TYPE_ARRAY), + ...parts, + ]); } } @@ -247,7 +255,10 @@ export class ObjectCodec { return new Link(refId, [new Link(ObjectCodec.TYPE_OBJECT), ...parts]); } else { // Wrap in a type marker for objects without IDs: (object (key val) ...) - return new Link(undefined, [new Link(ObjectCodec.TYPE_OBJECT), ...parts]); + return new Link(undefined, [ + new Link(ObjectCodec.TYPE_OBJECT), + ...parts, + ]); } } @@ -366,7 +377,7 @@ export class ObjectCodec { // Decode from base64 try { return Buffer.from(b64Str, 'base64').toString('utf-8'); - } catch (e) { + } catch { // If decode fails, return the raw value return b64Str; } @@ -379,7 +390,7 @@ export class ObjectCodec { // New format with self-reference: (obj_0: array item1 item2 ...) // Old format (for backward compatibility): (array obj_id item1 item2 ...) let startIdx = 1; - let arrayId = selfRefId; // Use self-reference ID from link.id if present + let arrayId = selfRefId; // Use self-reference ID from link.id if present // Check for old format with obj_id as second element if (!arrayId && link.values.length > 1) { @@ -407,7 +418,7 @@ export class ObjectCodec { // New format with self-reference: (obj_0: object (key val) ...) // Old format (for backward compatibility): (object obj_id (key val) ...) let startIdx = 1; - let objectId = selfRefId; // Use self-reference ID from link.id if present + let objectId = selfRefId; // Use self-reference ID from link.id if present // Check for old format with obj_id as second element if (!objectId && link.values.length > 1) { diff --git a/js/src/format.js b/js/src/format.js index d30cde2..280a5f6 100644 --- a/js/src/format.js +++ b/js/src/format.js @@ -90,7 +90,9 @@ export function escapeReference(options = {}) { */ export function unescapeReference(options = {}) { const { str } = options; - if (!str) return str; + if (!str) { + return str; + } // Unescape doubled quotes (Links Notation escape sequences) let unescaped = str.replace(/""/g, '"'); // "" -> " @@ -139,7 +141,7 @@ export function jsonToLino(options = {}) { if (json.length === 0) { return '()'; } - const elements = json.map(item => jsonToLino({ json: item })); + const elements = json.map((item) => jsonToLino({ json: item })); return `(${elements.join(' ')})`; } @@ -175,11 +177,17 @@ function parseReference(ref) { const str = String(ref); // Try boolean - if (str === 'true') return true; - if (str === 'false') return false; + if (str === 'true') { + return true; + } + if (str === 'false') { + return false; + } // Try null - if (str === 'null') return null; + if (str === 'null') { + return null; + } // Try number const num = Number(str); @@ -212,18 +220,32 @@ function convertParsedToJson(element) { } // If element is a link (has non-empty values) - if (element.values && Array.isArray(element.values) && element.values.length > 0) { + if ( + element.values && + Array.isArray(element.values) && + element.values.length > 0 + ) { // Simple rule: If link contains pairs (all children are 2-element links), it's an object // Otherwise, it's an array - const allPairs = element.values.every(child => { + const allPairs = element.values.every((child) => { // Must be a link with exactly 2 elements - if (!child.values || !Array.isArray(child.values) || child.values.length !== 2) { + if ( + !child.values || + !Array.isArray(child.values) || + child.values.length !== 2 + ) { return false; } // First element (key) must be a primitive const keyElement = child.values[0]; - if (!(keyElement.id !== undefined && keyElement.values && keyElement.values.length === 0)) { + if ( + !( + keyElement.id !== undefined && + keyElement.values && + keyElement.values.length === 0 + ) + ) { return false; } // Key should be string-like (not a pure number) @@ -246,7 +268,7 @@ function convertParsedToJson(element) { } // Not pairs, so it's an array - return element.values.map(v => convertParsedToJson(v)); + return element.values.map((v) => convertParsedToJson(v)); } return null; @@ -303,8 +325,10 @@ export function linoToJson(options = {}) { */ export function formatAsLino(options = {}) { const { values } = options; - if (!values || values.length === 0) return '()'; + if (!values || values.length === 0) { + return '()'; + } - const formattedValues = values.map(value => ` ${value}`).join('\n'); + const formattedValues = values.map((value) => ` ${value}`).join('\n'); return `(\n${formattedValues}\n)`; } diff --git a/js/src/fuzzy-match.js b/js/src/fuzzy-match.js index d139a0b..17dfae3 100644 --- a/js/src/fuzzy-match.js +++ b/js/src/fuzzy-match.js @@ -61,7 +61,9 @@ export function levenshteinDistance(options = {}) { export function stringSimilarity(options = {}) { const { a, b } = options; const maxLength = Math.max(a.length, b.length); - if (maxLength === 0) return 1.0; + if (maxLength === 0) { + return 1.0; + } const distance = levenshteinDistance({ a, b }); return 1 - distance / maxLength; @@ -104,7 +106,7 @@ export function extractKeywords(options = {}) { const words = normalized.split(/\s+/); const keywords = new Set( - words.filter(word => word.length > minWordLength && !stopwords.has(word)) + words.filter((word) => word.length > minWordLength && !stopwords.has(word)) ); // Add stems for longer words to improve matching @@ -137,10 +139,14 @@ export function keywordSimilarity(options = {}) { const keywordsA = extractKeywords({ question: a, ...options }); const keywordsB = extractKeywords({ question: b, ...options }); - if (keywordsA.size === 0 && keywordsB.size === 0) return 1.0; - if (keywordsA.size === 0 || keywordsB.size === 0) return 0.0; + if (keywordsA.size === 0 && keywordsB.size === 0) { + return 1.0; + } + if (keywordsA.size === 0 || keywordsB.size === 0) { + return 0.0; + } - const intersection = new Set([...keywordsA].filter(x => keywordsB.has(x))); + const intersection = new Set([...keywordsA].filter((x) => keywordsB.has(x))); const union = new Set([...keywordsA, ...keywordsB]); return intersection.size / union.size; @@ -176,10 +182,18 @@ export function findBestMatch(options = {}) { let bestScore = threshold; for (const [dbQuestion, answer] of qaDatabase.entries()) { - const editSimilarity = stringSimilarity({ a: normalizeQuestion({ question }), b: normalizeQuestion({ question: dbQuestion }) }); - const kwSimilarity = keywordSimilarity({ a: question, b: dbQuestion, ...options }); + const editSimilarity = stringSimilarity({ + a: normalizeQuestion({ question }), + b: normalizeQuestion({ question: dbQuestion }), + }); + const kwSimilarity = keywordSimilarity({ + a: question, + b: dbQuestion, + ...options, + }); - const combinedScore = editSimilarity * editWeight + kwSimilarity * keywordWeight; + const combinedScore = + editSimilarity * editWeight + kwSimilarity * keywordWeight; if (combinedScore > bestScore) { bestScore = combinedScore; @@ -220,9 +234,13 @@ export function findAllMatches(options = {}) { } else { const editSimilarity = stringSimilarity({ a: normalizeQuestion({ question }), - b: normalizeQuestion({ question: dbQuestion }) + b: normalizeQuestion({ question: dbQuestion }), + }); + const kwSimilarity = keywordSimilarity({ + a: question, + b: dbQuestion, + ...options, }); - const kwSimilarity = keywordSimilarity({ a: question, b: dbQuestion, ...options }); score = editSimilarity * editWeight + kwSimilarity * keywordWeight; } diff --git a/js/tests/test_circular_references.test.js b/js/tests/test_circular_references.test.js index ed4908f..36f81d0 100644 --- a/js/tests/test_circular_references.test.js +++ b/js/tests/test_circular_references.test.js @@ -60,7 +60,7 @@ test('self-referencing object', () => { const obj = { name: 'root' }; obj.self = obj; // Circular reference - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); assert.ok(encoded); assert.equal(typeof encoded, 'string'); @@ -76,7 +76,7 @@ test('object with multiple self-references', () => { obj.ref2 = obj; obj.ref3 = obj; - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); assert.equal(decoded.name, 'root'); @@ -88,7 +88,7 @@ test('object with multiple self-references', () => { test('nested object with circular reference', () => { const child = { name: 'child' }; - const parent = { name: 'parent', child: child }; + const parent = { name: 'parent', child }; child.parent = parent; // Create circular reference const encoded = encode({ obj: parent }); @@ -203,7 +203,7 @@ test('circular and shared references combined', () => { }; obj.self = obj; // Add circular reference - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); assert.equal(decoded.name, 'root'); @@ -232,7 +232,7 @@ test('array and object circular reference', () => { const arr = [1, obj]; obj.arr = arr; // obj -> arr -> obj - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); assert.equal(decoded.name, 'obj'); diff --git a/js/tests/test_collections.test.js b/js/tests/test_collections.test.js index 738a3aa..983541c 100644 --- a/js/tests/test_collections.test.js +++ b/js/tests/test_collections.test.js @@ -64,8 +64,15 @@ test('roundtrip array with mixed types', () => { test('nested arrays', () => { const testArrays = [ - [[1, 2], [3, 4]], - [[1, 2], [3, 4], [5, [6, 7]]], + [ + [1, 2], + [3, 4], + ], + [ + [1, 2], + [3, 4], + [5, [6, 7]], + ], [[[1]], [[2]], [[3]]], ]; for (const array of testArrays) { @@ -104,7 +111,7 @@ test('encode object with basic types', () => { score: 95.5, empty: null, }; - const result = encode({ obj: obj }); + const result = encode({ obj }); assert.ok(result); assert.equal(typeof result, 'string'); }); @@ -115,7 +122,7 @@ test('decode object with basic types', () => { age: 30, active: true, }; - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const result = decode({ notation: encoded }); assert.equal(typeof result, 'object'); assert.equal(result.name, 'Alice'); @@ -132,7 +139,7 @@ test('roundtrip object with mixed types', () => { { x: null, y: undefined, z: false }, ]; for (const obj of testObjects) { - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); assert.deepEqual(decoded, obj); } @@ -154,7 +161,7 @@ test('nested objects', () => { }, ]; for (const obj of testObjects) { - const encoded = encode({ obj: obj }); + const encoded = encode({ obj }); const decoded = decode({ notation: encoded }); assert.deepEqual(decoded, obj); } diff --git a/js/tests/test_format.test.js b/js/tests/test_format.test.js index 2bba12d..cd61e24 100644 --- a/js/tests/test_format.test.js +++ b/js/tests/test_format.test.js @@ -44,7 +44,7 @@ test('escapeReference - string with single quotes', () => { test('escapeReference - string with double quotes', () => { const result = escapeReference({ value: 'he said "hello"' }); assert.ok(result.startsWith("'")); - assert.equal(result, "'he said \"hello\"'"); + assert.equal(result, '\'he said "hello"\''); }); test('escapeReference - string with both quotes', () => { @@ -74,7 +74,10 @@ test('unescapeReference - simple string', () => { }); test('unescapeReference - doubled double quotes', () => { - assert.equal(unescapeReference({ str: 'he said ""hello""' }), 'he said "hello"'); + assert.equal( + unescapeReference({ str: 'he said ""hello""' }), + 'he said "hello"' + ); }); test('unescapeReference - doubled single quotes', () => { @@ -128,7 +131,15 @@ test('jsonToLino - array of strings', () => { }); test('jsonToLino - nested array', () => { - assert.equal(jsonToLino({ json: [[1, 2], [3, 4]] }), '((1 2) (3 4))'); + assert.equal( + jsonToLino({ + json: [ + [1, 2], + [3, 4], + ], + }), + '((1 2) (3 4))' + ); }); test('jsonToLino - empty object', () => { @@ -195,21 +206,21 @@ test('linoToJson - object with array value', () => { test('roundtrip - simple object', () => { const original = { name: 'Alice', age: 30 }; const lino = jsonToLino({ json: original }); - const result = linoToJson({ lino: lino }); + const result = linoToJson({ lino }); assert.deepEqual(result, original); }); test('roundtrip - nested object', () => { const original = { user: { name: 'Bob', active: true } }; const lino = jsonToLino({ json: original }); - const result = linoToJson({ lino: lino }); + const result = linoToJson({ lino }); assert.deepEqual(result, original); }); test('roundtrip - object with array', () => { const original = { items: [1, 2, 3] }; const lino = jsonToLino({ json: original }); - const result = linoToJson({ lino: lino }); + const result = linoToJson({ lino }); assert.deepEqual(result, original); }); @@ -222,7 +233,7 @@ test('roundtrip - complex object', () => { nested: { x: 1, y: 2 }, }; const lino = jsonToLino({ json: original }); - const result = linoToJson({ lino: lino }); + const result = linoToJson({ lino }); assert.deepEqual(result, original); }); diff --git a/js/tests/test_fuzzy_match.test.js b/js/tests/test_fuzzy_match.test.js index fe73cb3..e7be31f 100644 --- a/js/tests/test_fuzzy_match.test.js +++ b/js/tests/test_fuzzy_match.test.js @@ -72,25 +72,36 @@ describe('normalizeQuestion', () => { }); test('should remove punctuation', () => { - assert.equal(normalizeQuestion({ question: 'Hello, World!' }), 'hello world'); + assert.equal( + normalizeQuestion({ question: 'Hello, World!' }), + 'hello world' + ); assert.equal(normalizeQuestion({ question: 'What?!' }), 'what'); assert.equal(normalizeQuestion({ question: 'a.b.c' }), 'abc'); }); test('should standardize whitespace', () => { - assert.equal(normalizeQuestion({ question: 'hello world' }), 'hello world'); + assert.equal( + normalizeQuestion({ question: 'hello world' }), + 'hello world' + ); assert.equal(normalizeQuestion({ question: ' hello ' }), 'hello'); assert.equal(normalizeQuestion({ question: 'a b c' }), 'a b c'); }); test('should handle combined normalization', () => { - assert.equal(normalizeQuestion({ question: ' HELLO, World! What? ' }), 'hello world what'); + assert.equal( + normalizeQuestion({ question: ' HELLO, World! What? ' }), + 'hello world what' + ); }); }); describe('extractKeywords', () => { test('should extract all keywords when no stopwords provided', () => { - const keywords = extractKeywords({ question: 'What is the best way to learn programming?' }); + const keywords = extractKeywords({ + question: 'What is the best way to learn programming?', + }); assert.ok(keywords.has('best')); assert.ok(keywords.has('way')); assert.ok(keywords.has('learn')); @@ -103,14 +114,20 @@ describe('extractKeywords', () => { test('should filter out English stopwords when provided', () => { const stopwords = new Set(['the', 'and', 'or', 'is', 'are']); - const keywords = extractKeywords({ question: 'the and or is are', stopwords }); + const keywords = extractKeywords({ + question: 'the and or is are', + stopwords, + }); // All stopwords should be filtered out assert.equal(keywords.size, 0); }); test('should filter out Russian stopwords when provided', () => { const stopwords = new Set(['как', 'что', 'это', 'в', 'на']); - const keywords = extractKeywords({ question: 'как что это в на', stopwords }); + const keywords = extractKeywords({ + question: 'как что это в на', + stopwords, + }); assert.equal(keywords.size, 0); }); @@ -121,7 +138,10 @@ describe('extractKeywords', () => { }); test('should respect minWordLength option', () => { - const keywords = extractKeywords({ question: 'a ab abc abcd', minWordLength: 3 }); + const keywords = extractKeywords({ + question: 'a ab abc abcd', + minWordLength: 3, + }); assert.ok(!keywords.has('a')); assert.ok(!keywords.has('ab')); assert.ok(!keywords.has('abc')); // length 3, not > 3 @@ -129,7 +149,10 @@ describe('extractKeywords', () => { }); test('should disable stemming when stemLength is 0', () => { - const keywords = extractKeywords({ question: 'programming', stemLength: 0 }); + const keywords = extractKeywords({ + question: 'programming', + stemLength: 0, + }); assert.ok(keywords.has('programming')); assert.ok(!keywords.has('progr')); }); @@ -137,26 +160,35 @@ describe('extractKeywords', () => { describe('keywordSimilarity', () => { test('should return 1 for identical questions', () => { - const similarity = keywordSimilarity({ a: 'What is programming?', b: 'What is programming?' - }); + const similarity = keywordSimilarity({ + a: 'What is programming?', + b: 'What is programming?', + }); assert.equal(similarity, 1.0); }); test('should return positive similarity for similar questions', () => { - const similarity = keywordSimilarity({ a: 'What is programming?', b: 'What is computer programming?' - }); + const similarity = keywordSimilarity({ + a: 'What is programming?', + b: 'What is computer programming?', + }); // Both questions share "programming" keyword, so similarity should be positive assert.ok(similarity > 0); }); test('should return low similarity for different questions', () => { - const similarity = keywordSimilarity({ a: 'What is programming?', b: 'How is the weather today?' - }); + const similarity = keywordSimilarity({ + a: 'What is programming?', + b: 'How is the weather today?', + }); assert.ok(similarity < 0.5); }); test('should return 0 for completely different questions', () => { - const similarity = keywordSimilarity({ a: 'foo bar baz', b: 'xyz abc def' }); + const similarity = keywordSimilarity({ + a: 'foo bar baz', + b: 'xyz abc def', + }); assert.equal(similarity, 0); }); }); @@ -165,12 +197,18 @@ describe('findBestMatch', () => { const testDatabase = new Map([ ['What is your name?', 'My name is Claude'], ['How old are you?', "I don't have an age"], - ['What programming languages do you know?', 'I can help with many languages'], + [ + 'What programming languages do you know?', + 'I can help with many languages', + ], ['Как вас зовут?', 'Меня зовут Клод'], ]); test('should return exact match with score 1.0', () => { - const result = findBestMatch({ question: 'What is your name?', qaDatabase: testDatabase }); + const result = findBestMatch({ + question: 'What is your name?', + qaDatabase: testDatabase, + }); assert.equal(result.question, 'What is your name?'); assert.equal(result.score, 1.0); assert.equal(result.answer, 'My name is Claude'); @@ -178,25 +216,38 @@ describe('findBestMatch', () => { test('should find similar question with low threshold', () => { // Use a lower threshold since the similarity depends on keyword overlap - const result = findBestMatch({ question: 'What is your age?', qaDatabase: testDatabase, threshold: 0.2 }); + const result = findBestMatch({ + question: 'What is your age?', + qaDatabase: testDatabase, + threshold: 0.2, + }); assert.ok(result !== null); assert.ok(result.score > 0.2); }); test('should return null when no match above threshold', () => { - const result = findBestMatch({ question: 'Random unrelated question about weather', qaDatabase: testDatabase, - threshold: 0.9 + const result = findBestMatch({ + question: 'Random unrelated question about weather', + qaDatabase: testDatabase, + threshold: 0.9, }); assert.equal(result, null); }); test('should respect threshold option', () => { - const result = findBestMatch({ question: 'name?', qaDatabase: testDatabase, threshold: 0.1 }); + const result = findBestMatch({ + question: 'name?', + qaDatabase: testDatabase, + threshold: 0.1, + }); assert.ok(result !== null); }); test('should handle Russian questions', () => { - const result = findBestMatch({ question: 'Как вас зовут?', qaDatabase: testDatabase }); + const result = findBestMatch({ + question: 'Как вас зовут?', + qaDatabase: testDatabase, + }); assert.equal(result.question, 'Как вас зовут?'); assert.equal(result.score, 1.0); }); @@ -211,27 +262,41 @@ describe('findAllMatches', () => { ]); test('should return all matches above threshold', () => { - const matches = findAllMatches({ question: 'What is your name?', qaDatabase: testDatabase, threshold: 0.3 }); + const matches = findAllMatches({ + question: 'What is your name?', + qaDatabase: testDatabase, + threshold: 0.3, + }); assert.ok(matches.length >= 1); assert.equal(matches[0].score, 1.0); // Exact match should be first }); test('should sort matches by score descending', () => { - const matches = findAllMatches({ question: 'What is your name?', qaDatabase: testDatabase, threshold: 0.1 }); + const matches = findAllMatches({ + question: 'What is your name?', + qaDatabase: testDatabase, + threshold: 0.1, + }); for (let i = 1; i < matches.length; i++) { assert.ok(matches[i - 1].score >= matches[i].score); } }); test('should return empty array when no matches', () => { - const matches = findAllMatches({ question: 'Completely unrelated question', qaDatabase: testDatabase, - threshold: 0.9 + const matches = findAllMatches({ + question: 'Completely unrelated question', + qaDatabase: testDatabase, + threshold: 0.9, }); assert.deepEqual(matches, []); }); test('should include answer in results', () => { - const matches = findAllMatches({ question: 'What is your name?', qaDatabase: testDatabase, threshold: 0.9 }); + const matches = findAllMatches({ + question: 'What is your name?', + qaDatabase: testDatabase, + threshold: 0.9, + }); assert.ok(matches.length > 0); assert.equal(matches[0].answer, 'Claude'); }); diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml new file mode 100644 index 0000000..36a6207 --- /dev/null +++ b/python/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.13.0 + hooks: + - id: mypy + additional_dependencies: [pytest, pytest-asyncio] + args: [--strict, --ignore-missing-imports] diff --git a/python/.ruff.toml b/python/.ruff.toml new file mode 100644 index 0000000..e6a459e --- /dev/null +++ b/python/.ruff.toml @@ -0,0 +1,5 @@ +# Ruff configuration +# This file provides additional settings beyond pyproject.toml + +[lint.isort] +known-first-party = ["my_package"] diff --git a/python/changelog.d/20251218_133759_drakonard_issue_1_3b50e2f12be6.md b/python/changelog.d/20251218_133759_drakonard_issue_1_3b50e2f12be6.md new file mode 100644 index 0000000..2f15a70 --- /dev/null +++ b/python/changelog.d/20251218_133759_drakonard_issue_1_3b50e2f12be6.md @@ -0,0 +1,7 @@ +### Added + +- Scriv for changelog management (Python equivalent of Changesets) +- Fragment-based changelog workflow to prevent merge conflicts +- CI check for changelog fragments in pull requests +- Automated fragment collection in release workflow +- Documentation for changelog workflow in CONTRIBUTING.md and README.md diff --git a/python/changelog.d/README.md b/python/changelog.d/README.md new file mode 100644 index 0000000..fc68734 --- /dev/null +++ b/python/changelog.d/README.md @@ -0,0 +1,57 @@ +# Changelog Fragments + +This directory contains changelog fragments that will be collected into `CHANGELOG.md` during releases. + +## How to Add a Changelog Fragment + +When making changes that should be documented in the changelog, create a fragment file: + +```bash +# Create a new fragment (recommended - auto-generates filename with branch/timestamp) +scriv create + +# Or manually create a file matching the pattern: YYYYMMDD_HHMMSS_username.md +``` + +## Fragment Format + +Each fragment should contain relevant sections. Uncomment and fill in the appropriate sections: + +```markdown +### Added +- Description of new feature + +### Changed +- Description of change to existing functionality + +### Fixed +- Description of bug fix + +### Removed +- Description of removed feature + +### Deprecated +- Description of deprecated feature + +### Security +- Description of security fix +``` + +## Why Fragments? + +Using changelog fragments (similar to [Changesets](https://github.com/changesets/changesets) in JavaScript): + +1. **No merge conflicts**: Multiple PRs can add fragments without conflicts +2. **Per-PR documentation**: Each PR documents its own changes +3. **Automated collection**: Fragments are automatically collected during release +4. **Consistent format**: Template ensures consistent changelog entries + +## During Release + +Fragments are automatically collected into `CHANGELOG.md` by running: + +```bash +scriv collect --version X.Y.Z +``` + +This is handled automatically by the release workflow. diff --git a/python/changelog.d/fragment_template.md.j2 b/python/changelog.d/fragment_template.md.j2 new file mode 100644 index 0000000..62d6e5b --- /dev/null +++ b/python/changelog.d/fragment_template.md.j2 @@ -0,0 +1,31 @@ + diff --git a/python/pyproject.toml b/python/pyproject.toml index 5a360e8..86e6f12 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -28,6 +28,7 @@ dev = [ "pytest-cov>=4.0", "ruff>=0.1.0", "mypy>=1.0", + "scriv[toml]>=1.7.0", ] [project.urls] @@ -58,3 +59,31 @@ warn_return_any = false warn_unused_configs = true disallow_untyped_defs = false ignore_missing_imports = true + +# Scriv configuration for changelog management +# Similar to @changesets/cli but for Python projects +[tool.scriv] +# Fragment format (markdown for better GitHub compatibility) +format = "md" +# Fragment directory +fragment_directory = "changelog.d" +# Changelog file +output_file = "CHANGELOG.md" +# Categories for changelog entries +categories = [ + "Removed", + "Added", + "Changed", + "Deprecated", + "Fixed", + "Security", +] +# Version header format +entry_title_template = "## [{{ version }}] - {{ date.strftime('%Y-%m-%d') }}" +# Insert marker (where new entries go) +insert_marker = "" +# Main branch name +main_branches = ["main"] +# New fragment template +new_fragment_template = """file:changelog.d/fragment_template.md.j2 +""" diff --git a/python/scripts/bump_version.py b/python/scripts/bump_version.py new file mode 100755 index 0000000..6850af3 --- /dev/null +++ b/python/scripts/bump_version.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Bump version in pyproject.toml and update CHANGELOG.md + +Usage: + python scripts/bump_version.py [--description "..."] + +Examples: + python scripts/bump_version.py patch + python scripts/bump_version.py minor --description "Add new feature" + python scripts/bump_version.py major --description "Breaking changes" +""" + +import argparse +import re +import sys +from datetime import datetime +from pathlib import Path + + +def get_current_version(pyproject_path: Path) -> str: + """Extract current version from pyproject.toml.""" + content = pyproject_path.read_text() + match = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE) + if not match: + raise ValueError("Could not find version in pyproject.toml") + return match.group(1) + + +def bump_version(current: str, bump_type: str) -> str: + """Bump version according to semantic versioning.""" + parts = current.split(".") + if len(parts) != 3: + raise ValueError(f"Invalid version format: {current}") + + major, minor, patch = map(int, parts) + + if bump_type == "major": + return f"{major + 1}.0.0" + elif bump_type == "minor": + return f"{major}.{minor + 1}.0" + elif bump_type == "patch": + return f"{major}.{minor}.{patch + 1}" + else: + raise ValueError(f"Invalid bump type: {bump_type}") + + +def update_pyproject(pyproject_path: Path, old_version: str, new_version: str) -> None: + """Update version in pyproject.toml.""" + content = pyproject_path.read_text() + pattern = rf'^(version\s*=\s*["\']){re.escape(old_version)}(["\'])' + new_content = re.sub( + pattern, rf"\g<1>{new_version}\g<2>", content, flags=re.MULTILINE + ) + + if content == new_content: + raise ValueError( + f"Failed to update version from {old_version} to {new_version}" + ) + + pyproject_path.write_text(new_content) + print(f"✓ Updated pyproject.toml: {old_version} → {new_version}") + + +def update_changelog( + changelog_path: Path, version: str, bump_type: str, description: str +) -> None: + """Update CHANGELOG.md with new version entry.""" + if not changelog_path.exists(): + print(f"Warning: {changelog_path} not found, skipping changelog update") + return + + content = changelog_path.read_text() + today = datetime.now().strftime("%Y-%m-%d") + + # Create new entry + bump_type_title = bump_type.capitalize() + new_entry = f"""## {version} - {today} + +### {bump_type_title} Changes + +- {description} + +""" + + # Find insertion point (after first heading, before first version section) + match = re.search(r"^## ", content, re.MULTILINE) + + if match: + # Insert before first version section + insert_pos = match.start() + new_content = content[:insert_pos] + new_entry + content[insert_pos:] + else: + # If no version sections, insert after main heading + main_heading_match = re.search(r"^# .+$", content, re.MULTILINE) + if main_heading_match: + insert_pos = main_heading_match.end() + new_content = ( + content[:insert_pos] + "\n\n" + new_entry + content[insert_pos:] + ) + else: + # Prepend if no headings at all + new_content = new_entry + "\n" + content + + changelog_path.write_text(new_content) + print(f"✓ Updated {changelog_path.name}") + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Bump version and update changelog", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "bump_type", + choices=["major", "minor", "patch"], + help="Type of version bump", + ) + parser.add_argument( + "--description", + "-d", + default="", + help="Description of changes for changelog", + ) + + args = parser.parse_args() + + # Determine project root and files + script_dir = Path(__file__).parent + project_root = script_dir.parent + pyproject_path = project_root / "pyproject.toml" + changelog_path = project_root / "CHANGELOG.md" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found", file=sys.stderr) + return 1 + + try: + # Get current version + old_version = get_current_version(pyproject_path) + print(f"Current version: {old_version}") + + # Calculate new version + new_version = bump_version(old_version, args.bump_type) + print(f"New version: {new_version}") + + # Update files + update_pyproject(pyproject_path, old_version, new_version) + + description = args.description or f"Manual {args.bump_type} release" + update_changelog(changelog_path, new_version, args.bump_type, description) + + print(f"\n✅ Version bump complete: {old_version} → {new_version}") + print("\nNext steps:") + print(" 1. Review changes: git diff") + print( + " 2. Commit: git add . && git commit -m 'chore: bump version to {new_version}'" + ) + print(" 3. Tag: git tag v{new_version}") + print(" 4. Push: git push && git push --tags") + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/check_file_size.py b/python/scripts/check_file_size.py new file mode 100755 index 0000000..e90ac79 --- /dev/null +++ b/python/scripts/check_file_size.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Check for files exceeding the maximum allowed line count. + +Exits with error code 1 if any files exceed the limit. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +MAX_LINES = 1000 +FILE_EXTENSIONS = [".py"] +EXCLUDE_PATTERNS = [ + "node_modules", + ".venv", + "venv", + "env", + "__pycache__", + ".git", + "build", + "dist", + ".eggs", + "*.egg-info", +] + + +def should_exclude(path: Path, exclude_patterns: list[str]) -> bool: + """Check if a path should be excluded. + + Args: + path: Path to check + exclude_patterns: List of patterns to exclude + + Returns: + True if path should be excluded + """ + path_str = str(path) + return any(pattern in path_str for pattern in exclude_patterns) + + +def find_python_files(directory: Path, exclude_patterns: list[str]) -> list[Path]: + """Recursively find all Python files in a directory. + + Args: + directory: Directory to search + exclude_patterns: Patterns to exclude + + Returns: + List of file paths + """ + files = [] + for path in directory.rglob("*"): + if should_exclude(path, exclude_patterns): + continue + if path.is_file() and path.suffix in FILE_EXTENSIONS: + files.append(path) + return files + + +def count_lines(file_path: Path) -> int: + """Count lines in a file. + + Args: + file_path: Path to the file + + Returns: + Number of lines + """ + return len(file_path.read_text(encoding="utf-8").split("\n")) + + +def main() -> None: + """Main function.""" + cwd = Path.cwd() + print(f"\nChecking Python files for maximum {MAX_LINES} lines...\n") + + files = find_python_files(cwd, EXCLUDE_PATTERNS) + violations = [] + + for file in files: + line_count = count_lines(file) + if line_count > MAX_LINES: + violations.append({"file": file.relative_to(cwd), "lines": line_count}) + + if not violations: + print("✓ All files are within the line limit\n") + sys.exit(0) + else: + print("✗ Found files exceeding the line limit:\n") + for violation in violations: + print( + f" {violation['file']}: {violation['lines']} lines " + f"(exceeds {MAX_LINES})" + ) + print(f"\nPlease refactor these files to be under {MAX_LINES} lines\n") + sys.exit(1) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + if "DEBUG" in sys.modules: + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/python/scripts/create_github_release.py b/python/scripts/create_github_release.py new file mode 100755 index 0000000..6d62d3a --- /dev/null +++ b/python/scripts/create_github_release.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Create a GitHub release from CHANGELOG.md content. + +Usage: + python scripts/create_github_release.py --version VERSION --repository REPO + +Example: + python scripts/create_github_release.py --version 1.2.3 --repository owner/repo + +Environment variables: + GH_TOKEN or GITHUB_TOKEN: GitHub token for authentication +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + + +def run_command(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: + """Run a command and handle errors.""" + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + if result.stdout: + print(result.stdout) + if result.stderr and result.returncode != 0: + print(result.stderr, file=sys.stderr) + + if check and result.returncode != 0: + print( + f"Error: Command failed with exit code {result.returncode}", + file=sys.stderr, + ) + sys.exit(result.returncode) + + return result + + +def extract_changelog_entry(changelog_path: Path, version: str) -> str: + """Extract the changelog entry for a specific version.""" + if not changelog_path.exists(): + print(f"Warning: {changelog_path} not found", file=sys.stderr) + return f"Release {version}" + + content = changelog_path.read_text() + + # Look for version section (e.g., "## 1.2.3" or "## 1.2.3 - 2024-01-15") + version_pattern = rf"^## {re.escape(version)}(\s|$)" + match = re.search(version_pattern, content, re.MULTILINE) + + if not match: + print( + f"Warning: Version {version} not found in {changelog_path}", + file=sys.stderr, + ) + return f"Release {version}" + + # Extract content until next version section or end of file + start = match.end() + next_version = re.search(r"^## \d+\.\d+\.\d+", content[start:], re.MULTILINE) + + if next_version: + entry = content[start : start + next_version.start()].strip() + else: + entry = content[start:].strip() + + return entry if entry else f"Release {version}" + + +def create_release( + version: str, repository: str, release_notes: str, prerelease: bool = False +) -> None: + """Create a GitHub release using gh CLI.""" + tag = f"v{version}" + + print(f"\nCreating GitHub release for {tag}...") + print(f"Repository: {repository}") + print(f"Prerelease: {prerelease}") + print(f"\nRelease notes:\n{release_notes}\n") + + cmd = [ + "gh", + "release", + "create", + tag, + "--repo", + repository, + "--title", + tag, + "--notes", + release_notes, + ] + + if prerelease: + cmd.append("--prerelease") + + run_command(cmd) + print(f"\n✅ GitHub release {tag} created successfully!") + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Create GitHub release from CHANGELOG.md", + ) + parser.add_argument( + "--version", + "-v", + required=True, + help="Version to release (e.g., 1.2.3)", + ) + parser.add_argument( + "--repository", + "-r", + required=True, + help="GitHub repository (owner/repo)", + ) + parser.add_argument( + "--prerelease", + action="store_true", + help="Mark as prerelease", + ) + + args = parser.parse_args() + + # Check for GitHub token + if not os.environ.get("GH_TOKEN") and not os.environ.get("GITHUB_TOKEN"): + print( + "Error: GH_TOKEN or GITHUB_TOKEN environment variable required", + file=sys.stderr, + ) + return 1 + + # Check if gh CLI is available + result = run_command(["gh", "--version"], check=False) + if result.returncode != 0: + print( + "Error: gh CLI not found. Install from https://cli.github.com/", + file=sys.stderr, + ) + return 1 + + # Determine project root + script_dir = Path(__file__).parent + project_root = script_dir.parent + changelog_path = project_root / "CHANGELOG.md" + + try: + # Extract changelog entry + release_notes = extract_changelog_entry(changelog_path, args.version) + + # Create release + create_release(args.version, args.repository, release_notes, args.prerelease) + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/create_manual_changeset.py b/python/scripts/create_manual_changeset.py new file mode 100644 index 0000000..083a88d --- /dev/null +++ b/python/scripts/create_manual_changeset.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +Create a manual changelog fragment for releases. + +This script is the Python equivalent of create-manual-changeset.mjs from the JS template. +It creates a changelog fragment in the changelog.d/ directory for documenting changes. + +Usage: + python scripts/create_manual_changeset.py [--description "..."] + +Examples: + python scripts/create_manual_changeset.py patch + python scripts/create_manual_changeset.py minor --description "Add new feature" + python scripts/create_manual_changeset.py major --description "Breaking changes" + +Note: This wraps 'scriv create' but can also create fragments manually if scriv +is not installed. +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +from datetime import datetime +from pathlib import Path + + +def get_branch_name() -> str: + """Get current git branch name.""" + try: + result = subprocess.run( + ["git", "branch", "--show-current"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return "manual" + + +def get_username() -> str: + """Get current user name for fragment filename.""" + # Try git user.name first + try: + result = subprocess.run( + ["git", "config", "user.name"], + capture_output=True, + text=True, + check=True, + ) + username = result.stdout.strip() + if username: + # Sanitize username for filename + return re.sub(r"[^a-zA-Z0-9_-]", "_", username).lower() + except (subprocess.CalledProcessError, FileNotFoundError): + pass + + # Fall back to environment variable or default + return os.environ.get("USER", os.environ.get("USERNAME", "user")).lower() + + +def has_scriv() -> bool: + """Check if scriv is installed.""" + return shutil.which("scriv") is not None + + +def create_with_scriv(bump_type: str, description: str) -> int: + """Create fragment using scriv create command.""" + print("Using scriv to create changelog fragment...") + + try: + result = subprocess.run( + ["scriv", "create"], + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + print(f"Warning: scriv create returned non-zero: {result.stderr}") + return result.returncode + + print(result.stdout) + + # Find the created fragment + changelog_dir = Path("changelog.d") + if changelog_dir.exists(): + fragments = sorted( + [ + f + for f in changelog_dir.glob("*.md") + if f.name != "README.md" and not f.name.endswith(".j2") + ], + key=lambda x: x.stat().st_mtime, + reverse=True, + ) + + if fragments: + fragment_path = fragments[0] + print(f"\nCreated fragment: {fragment_path}") + print("\nPlease edit the fragment file to document your changes.") + + if description: + # Update fragment with provided description + bump_category = { + "major": "Changed", # Major = breaking changes + "minor": "Added", # Minor = new features + "patch": "Fixed", # Patch = bug fixes + }.get(bump_type, "Changed") + + # Add the description under the appropriate category + new_content = f"### {bump_category}\n\n- {description}\n" + fragment_path.write_text(new_content) + print(f"Updated fragment with {bump_type} change: {description}") + + return 0 + + except FileNotFoundError: + print("Error: scriv command not found") + return 1 + + +def create_manual_fragment( + changelog_dir: Path, bump_type: str, description: str +) -> int: + """Create a changelog fragment manually without scriv.""" + # Generate filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + username = get_username() + branch = get_branch_name() + + # Sanitize branch name + safe_branch = re.sub(r"[^a-zA-Z0-9_-]", "_", branch) + + filename = f"{timestamp}_{username}_{safe_branch}.md" + fragment_path = changelog_dir / filename + + # Determine category based on bump type + bump_category = { + "major": "Changed", # Major = breaking changes + "minor": "Added", # Minor = new features + "patch": "Fixed", # Patch = bug fixes + }.get(bump_type, "Changed") + + # Create fragment content + if description: + content = f"### {bump_category}\n\n- {description}\n" + else: + content = """ + +### Added + +- New feature description + +### Changed + +- Change to existing functionality + +### Fixed + +- Bug fix description + + +""" + + fragment_path.write_text(content) + print(f"Created changelog fragment: {fragment_path}") + + if not description: + print("\nPlease edit the fragment file to document your changes.") + print(f" File: {fragment_path}") + + return 0 + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Create a changelog fragment for release documentation", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +This script creates a changelog fragment in changelog.d/ to document changes. +It's the Python equivalent of 'npx changeset' in JavaScript projects. + +The fragment will be collected into CHANGELOG.md during release. + """, + ) + parser.add_argument( + "bump_type", + choices=["major", "minor", "patch"], + help="Type of version bump (determines default category)", + ) + parser.add_argument( + "--description", + "-d", + default="", + help="Description of changes (optional, can edit file later)", + ) + parser.add_argument( + "--no-scriv", + action="store_true", + help="Create fragment manually without using scriv", + ) + + args = parser.parse_args() + + # Determine project root and changelog directory + script_dir = Path(__file__).parent + project_root = script_dir.parent + changelog_dir = project_root / "changelog.d" + + # Ensure changelog directory exists + if not changelog_dir.exists(): + changelog_dir.mkdir(parents=True) + print(f"Created directory: {changelog_dir}") + + # Use scriv if available, unless --no-scriv is specified + if has_scriv() and not args.no_scriv: + return create_with_scriv(args.bump_type, args.description) + else: + if not args.no_scriv: + print("Note: scriv not found, creating fragment manually") + print( + "Install scriv for better fragment management: pip install scriv[toml]" + ) + print() + return create_manual_fragment(changelog_dir, args.bump_type, args.description) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/format_release_notes.py b/python/scripts/format_release_notes.py new file mode 100644 index 0000000..e965087 --- /dev/null +++ b/python/scripts/format_release_notes.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +Format GitHub release notes with enhanced information. + +This script is the Python equivalent of format-release-notes.mjs from the JS template. +It enhances GitHub release notes with: +- PyPI version badge +- Link to associated pull request +- Clean formatting + +Usage: + python scripts/format_release_notes.py --release-id --version \\ + --repository [--commit-sha ] + +Example: + python scripts/format_release_notes.py --release-id 12345 --version 1.0.0 \\ + --repository link-foundation/my-package --commit-sha abc123 +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from typing import Optional + + +def run_gh_command(args: list[str]) -> tuple[bool, str]: + """Run a gh CLI command and return (success, output).""" + try: + result = subprocess.run( + ["gh"] + args, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False, result.stderr + return True, result.stdout + except FileNotFoundError: + return False, "gh CLI not found. Install from https://cli.github.com/" + + +def get_release_body(repository: str, release_id: str) -> tuple[bool, str]: + """Get the body of a GitHub release.""" + success, output = run_gh_command( + ["api", f"repos/{repository}/releases/{release_id}", "--jq", ".body"] + ) + return success, output.strip() if success else output + + +def find_pr_for_commit(repository: str, commit_sha: str) -> Optional[str]: + """Find the pull request that contains a specific commit.""" + if not commit_sha: + return None + + success, output = run_gh_command( + [ + "api", + f"repos/{repository}/commits/{commit_sha}/pulls", + "--jq", + ".[0].number", + ] + ) + + if success and output.strip(): + try: + pr_number = int(output.strip()) + return str(pr_number) + except ValueError: + pass + + return None + + +def format_release_body( + body: str, + version: str, + repository: str, + pr_number: Optional[str], + package_name: str, +) -> str: + """Format the release body with enhanced information.""" + # Check if already formatted (has PyPI badge) + if "pypi.org/project" in body.lower() or "img.shields.io" in body.lower(): + print("Release notes already formatted, skipping") + return body + + formatted_parts = [] + + # Add PyPI badge + pypi_badge = ( + f"[![PyPI version](https://img.shields.io/pypi/v/{package_name}.svg)]" + f"(https://pypi.org/project/{package_name}/)" + ) + formatted_parts.append(pypi_badge) + formatted_parts.append("") + + # Add PR link if available + if pr_number: + pr_link = f"**Pull Request:** [#{pr_number}](https://github.com/{repository}/pull/{pr_number})" + formatted_parts.append(pr_link) + formatted_parts.append("") + + # Clean up the existing body + cleaned_body = body.strip() + + # Fix escaped newlines and special characters + cleaned_body = cleaned_body.replace("\\n", "\n") + cleaned_body = cleaned_body.replace("\\r", "") + cleaned_body = cleaned_body.replace('\\"', '"') + + # Remove duplicate version headers if present + version_pattern = rf"^#+\s*v?{re.escape(version)}\s*$" + cleaned_body = re.sub(version_pattern, "", cleaned_body, flags=re.MULTILINE) + + # Clean up excessive whitespace + cleaned_body = re.sub(r"\n{3,}", "\n\n", cleaned_body) + cleaned_body = cleaned_body.strip() + + if cleaned_body: + formatted_parts.append(cleaned_body) + + return "\n".join(formatted_parts) + + +def update_release(repository: str, release_id: str, new_body: str) -> bool: + """Update the release body on GitHub.""" + # Use gh api to update the release + success, output = run_gh_command( + [ + "api", + "-X", + "PATCH", + f"repos/{repository}/releases/{release_id}", + "-f", + f"body={new_body}", + ] + ) + + if not success: + print(f"Error updating release: {output}", file=sys.stderr) + return False + + return True + + +def get_package_name() -> str: + """Get the package name from pyproject.toml.""" + try: + with open("pyproject.toml") as f: + content = f.read() + match = re.search(r'^name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE) + if match: + return match.group(1) + except FileNotFoundError: + pass + + return "my-package" + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Format GitHub release notes with enhanced information", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--release-id", + required=True, + help="GitHub release ID", + ) + parser.add_argument( + "--version", + required=True, + help="Release version (e.g., 1.0.0)", + ) + parser.add_argument( + "--repository", + required=True, + help="Repository in owner/repo format", + ) + parser.add_argument( + "--commit-sha", + default="", + help="Commit SHA to find associated PR", + ) + parser.add_argument( + "--package-name", + default="", + help="Package name for PyPI badge (auto-detected from pyproject.toml if not provided)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print formatted notes without updating release", + ) + + args = parser.parse_args() + + # Get package name + package_name = args.package_name or get_package_name() + print(f"Package name: {package_name}") + + # Get current release body + print(f"Fetching release {args.release_id}...") + success, body = get_release_body(args.repository, args.release_id) + if not success: + print(f"Error fetching release: {body}", file=sys.stderr) + return 1 + + print(f"Current body length: {len(body)} characters") + + # Find associated PR + pr_number = None + if args.commit_sha: + print(f"Looking for PR associated with commit {args.commit_sha}...") + pr_number = find_pr_for_commit(args.repository, args.commit_sha) + if pr_number: + print(f"Found PR: #{pr_number}") + else: + print("No associated PR found") + + # Format the release body + formatted_body = format_release_body( + body, + args.version, + args.repository, + pr_number, + package_name, + ) + + if args.dry_run: + print("\n--- Formatted Release Notes ---") + print(formatted_body) + print("--- End ---\n") + return 0 + + # Update release + if formatted_body != body: + print("Updating release notes...") + if update_release(args.repository, args.release_id, formatted_body): + print("Release notes updated successfully!") + return 0 + else: + return 1 + else: + print("No changes needed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/publish_to_pypi.py b/python/scripts/publish_to_pypi.py new file mode 100755 index 0000000..641bede --- /dev/null +++ b/python/scripts/publish_to_pypi.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Build and publish package to PyPI using trusted publishing (OIDC). + +This script: +1. Cleans previous build artifacts +2. Builds the package using hatchling +3. Validates the built distribution +4. Publishes to PyPI using OIDC (no token needed in CI) + +Usage: + python scripts/publish_to_pypi.py [--dry-run] + +Note: In GitHub Actions, this uses OIDC trusted publishing. + For local testing, use --dry-run or set TWINE_USERNAME/TWINE_PASSWORD. +""" + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + + +def run_command(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: + """Run a command and handle errors.""" + print(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + + if check and result.returncode != 0: + print( + f"Error: Command failed with exit code {result.returncode}", file=sys.stderr + ) + sys.exit(result.returncode) + + return result + + +def clean_build_artifacts(project_root: Path) -> None: + """Remove previous build artifacts.""" + print("Cleaning build artifacts...") + dirs_to_remove = ["dist", "build", "*.egg-info"] + + for pattern in dirs_to_remove: + if "*" in pattern: + for path in project_root.glob(pattern): + if path.is_dir(): + shutil.rmtree(path) + print(f" Removed: {path}") + else: + path = project_root / pattern + if path.exists(): + shutil.rmtree(path) + print(f" Removed: {path}") + + +def build_package(project_root: Path) -> None: + """Build the package using python -m build.""" + print("\nBuilding package...") + run_command([sys.executable, "-m", "build", str(project_root)]) + + +def check_package(dist_dir: Path) -> None: + """Validate the built package using twine.""" + print("\nValidating package...") + dist_files = list(dist_dir.glob("*")) + + if not dist_files: + print("Error: No distribution files found in dist/", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(dist_files)} distribution file(s):") + for file in dist_files: + print(f" - {file.name}") + + run_command([sys.executable, "-m", "twine", "check"] + [str(f) for f in dist_files]) + + +def publish_package(dist_dir: Path, dry_run: bool = False) -> None: + """Publish package to PyPI.""" + dist_files = list(dist_dir.glob("*")) + + if not dist_files: + print("Error: No distribution files found in dist/", file=sys.stderr) + sys.exit(1) + + if dry_run: + print("\n[DRY RUN] Would publish the following files:") + for file in dist_files: + print(f" - {file.name}") + print("\nSkipping actual upload (dry run mode)") + return + + print("\nPublishing to PyPI...") + + # Use twine upload with OIDC if in CI, otherwise use credentials + cmd = [sys.executable, "-m", "twine", "upload"] + cmd.extend([str(f) for f in dist_files]) + + run_command(cmd) + print("\n✅ Package published successfully!") + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Build and publish package to PyPI", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Build and validate but don't publish", + ) + + args = parser.parse_args() + + # Determine project root + script_dir = Path(__file__).parent + project_root = script_dir.parent + dist_dir = project_root / "dist" + + try: + # Ensure required tools are available + for tool in ["build", "twine"]: + result = run_command( + [sys.executable, "-m", tool, "--version"], + check=False, + ) + if result.returncode != 0: + print( + f"Error: {tool} is not installed. Install with: pip install {tool}", + file=sys.stderr, + ) + return 1 + + # Clean, build, check + clean_build_artifacts(project_root) + build_package(project_root) + check_package(dist_dir) + + # Publish (unless dry run) + publish_package(dist_dir, dry_run=args.dry_run) + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/validate_changeset.py b/python/scripts/validate_changeset.py new file mode 100644 index 0000000..c968488 --- /dev/null +++ b/python/scripts/validate_changeset.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Validate that PRs contain proper changelog fragments. + +This script is the Python equivalent of validate-changeset.mjs from the JS template. +It ensures that pull requests include changelog documentation. + +Usage: + python scripts/validate_changeset.py + +Exit codes: + 0 - Validation passed (fragment found or no source changes) + 1 - Validation failed (source changes without fragment) + +Example CI usage: + - name: Validate changelog fragment + run: python scripts/validate_changeset.py +""" + +import re +import sys +from pathlib import Path + + +def get_fragment_files(changelog_dir: Path) -> list[Path]: + """Get list of changelog fragment files (excluding README and template).""" + if not changelog_dir.exists(): + return [] + + return [ + f + for f in changelog_dir.glob("*.md") + if f.name != "README.md" and not f.name.endswith(".j2") + ] + + +def validate_fragment_content(fragment_path: Path) -> tuple[bool, str]: + """ + Validate that a fragment has proper content. + + Returns (is_valid, error_message). + """ + content = fragment_path.read_text().strip() + + if not content: + return False, f"Fragment {fragment_path.name} is empty" + + # Check for at least one category heading + category_pattern = re.compile( + r"^###\s*(Added|Changed|Deprecated|Fixed|Removed|Security)", + re.MULTILINE | re.IGNORECASE, + ) + + if not category_pattern.search(content): + return False, ( + f"Fragment {fragment_path.name} missing category heading.\n" + "Expected one of: ### Added, ### Changed, ### Deprecated, " + "### Fixed, ### Removed, ### Security" + ) + + # Check for actual content (not just commented template) + # Remove HTML comments + content_without_comments = re.sub(r"", "", content, flags=re.DOTALL) + # Check if there's meaningful content after headings + lines = [ + line.strip() + for line in content_without_comments.split("\n") + if line.strip() and not line.strip().startswith("#") + ] + + if not lines: + return False, ( + f"Fragment {fragment_path.name} has no content.\n" + "Please add a description of your changes under the appropriate category." + ) + + return True, "" + + +def main() -> int: + """Main entry point.""" + # Determine project root and changelog directory + script_dir = Path(__file__).parent + project_root = script_dir.parent + changelog_dir = project_root / "changelog.d" + + print("Validating changelog fragments...") + print() + + # Get fragment files + fragments = get_fragment_files(changelog_dir) + fragment_count = len(fragments) + + print(f"Found {fragment_count} changelog fragment(s)") + + if fragment_count == 0: + print() + print("WARNING: No changelog fragment found!") + print() + print("To document your changes, create a changelog fragment:") + print() + print(" # Using scriv (recommended):") + print(" pip install 'scriv[toml]'") + print(" scriv create") + print() + print(" # Or using the helper script:") + print( + " python scripts/create_manual_changeset.py patch --description 'Your changes'" + ) + print() + print("See changelog.d/README.md for more information.") + print() + + # This is currently a warning, not a failure + # Change to "return 1" to make it required + return 0 + + if fragment_count > 1: + print() + print( + f"WARNING: Found {fragment_count} fragments. Usually PRs should have only one." + ) + print("Fragments found:") + for f in fragments: + print(f" - {f.name}") + print() + + # Validate each fragment + all_valid = True + for fragment in fragments: + is_valid, error = validate_fragment_content(fragment) + if is_valid: + print(f" [OK] {fragment.name}") + else: + print(f" [FAIL] {error}") + all_valid = False + + print() + + if all_valid: + print("Changelog validation passed!") + return 0 + else: + print("Changelog validation FAILED!") + print() + print("Expected fragment format:") + print() + print(" ### Added") + print(" - Description of new feature") + print() + print(" ### Changed") + print(" - Description of change") + print() + print(" ### Fixed") + print(" - Description of bug fix") + print() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/scripts/version_and_commit.py b/python/scripts/version_and_commit.py new file mode 100755 index 0000000..5080e82 --- /dev/null +++ b/python/scripts/version_and_commit.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +Version packages and commit to main branch. + +This script handles version bumping and committing for CI/CD workflows. +It supports idempotent re-runs and detects when work was already completed. + +Usage: + python scripts/version_and_commit.py --bump-type [--description "..."] + +Example: + python scripts/version_and_commit.py --bump-type patch + python scripts/version_and_commit.py --bump-type minor --description "New feature" + +Environment variables: + GITHUB_OUTPUT: Path to GitHub Actions output file +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + + +def run_command( + cmd: list[str], check: bool = True, capture: bool = False +) -> subprocess.CompletedProcess: + """Run a command and handle errors.""" + cmd_str = " ".join(cmd) + print(f"Running: {cmd_str}") + + result = subprocess.run( + cmd, + capture_output=capture, + text=True, + check=False, + ) + + if not capture: + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + + if check and result.returncode != 0: + if capture: + print(result.stdout) + print(result.stderr, file=sys.stderr) + print( + f"Error: Command failed with exit code {result.returncode}", + file=sys.stderr, + ) + sys.exit(result.returncode) + + return result + + +def set_github_output(key: str, value: str) -> None: + """Set GitHub Actions output variable.""" + output_file = os.environ.get("GITHUB_OUTPUT") + if output_file: + with open(output_file, "a") as f: + f.write(f"{key}={value}\n") + print(f"Set output: {key}={value}") + + +def get_current_version(pyproject_path: Path) -> str: + """Get version from pyproject.toml.""" + content = pyproject_path.read_text() + match = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE) + if not match: + raise ValueError("Could not find version in pyproject.toml") + return match.group(1) + + +def configure_git() -> None: + """Configure git for automated commits.""" + print("Configuring git...") + run_command( + ["git", "config", "user.name", "github-actions[bot]"], + ) + run_command( + ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"], + ) + + +def check_remote_changes(pyproject_path: Path) -> tuple[bool, str]: + """ + Check if remote main has advanced (handles re-runs). + Returns (already_released, remote_version). + """ + print("\nChecking for remote changes...") + run_command(["git", "fetch", "origin", "main"]) + + # Get commit SHAs + local_head = run_command( + ["git", "rev-parse", "HEAD"], + capture=True, + ).stdout.strip() + + remote_head = run_command( + ["git", "rev-parse", "origin/main"], + capture=True, + ).stdout.strip() + + if local_head != remote_head: + print(f"Remote main has advanced (local: {local_head}, remote: {remote_head})") + print("This may indicate a previous attempt partially succeeded.") + + # Get remote version + remote_content = run_command( + ["git", "show", "origin/main:pyproject.toml"], + capture=True, + ).stdout + + remote_match = re.search( + r'^version\s*=\s*["\']([^"\']+)["\']', + remote_content, + re.MULTILINE, + ) + if remote_match: + remote_version = remote_match.group(1) + print(f"Remote version: {remote_version}") + + # Check if versions differ (indicating work was done) + local_version = get_current_version(pyproject_path) + if local_version != remote_version: + print("Local and remote versions differ, rebasing...") + run_command(["git", "rebase", "origin/main"]) + return False, remote_version + else: + print("Versions match, assuming previous run completed successfully") + return True, remote_version + + return False, "" + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Version bump and commit for CI/CD", + ) + parser.add_argument( + "--bump-type", + choices=["major", "minor", "patch"], + required=True, + help="Type of version bump", + ) + parser.add_argument( + "--description", + default="", + help="Description for changelog", + ) + + args = parser.parse_args() + + # Determine project root + script_dir = Path(__file__).parent + project_root = script_dir.parent + pyproject_path = project_root / "pyproject.toml" + + if not pyproject_path.exists(): + print(f"Error: {pyproject_path} not found", file=sys.stderr) + return 1 + + try: + # Configure git + configure_git() + + # Check for remote changes + already_released, remote_version = check_remote_changes(pyproject_path) + + if already_released: + print("Version bump already completed in previous run") + set_github_output("version_committed", "false") + set_github_output("already_released", "true") + set_github_output("new_version", remote_version) + return 0 + + # Get current version + old_version = get_current_version(pyproject_path) + print(f"\nCurrent version: {old_version}") + + # Run version bump + print(f"\nBumping version ({args.bump_type})...") + bump_cmd = [ + sys.executable, + "scripts/bump_version.py", + args.bump_type, + ] + if args.description: + bump_cmd.extend(["--description", args.description]) + + run_command(bump_cmd) + + # Get new version + new_version = get_current_version(pyproject_path) + print(f"New version: {new_version}") + set_github_output("new_version", new_version) + + # Check for changes + status = run_command( + ["git", "status", "--porcelain"], + capture=True, + ).stdout.strip() + + if status: + print("\nChanges detected, committing...") + + # Stage all changes + run_command(["git", "add", "-A"]) + + # Commit with version as message + run_command(["git", "commit", "-m", new_version]) + + # Push to main + run_command(["git", "push", "origin", "main"]) + + print( + f"\n✅ Version bump committed and pushed: {old_version} → {new_version}" + ) + set_github_output("version_committed", "true") + else: + print("\nNo changes to commit") + set_github_output("version_committed", "false") + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/src/link_notation_objects_codec/codec.py b/python/src/link_notation_objects_codec/codec.py index ea8762e..4ce055f 100644 --- a/python/src/link_notation_objects_codec/codec.py +++ b/python/src/link_notation_objects_codec/codec.py @@ -169,15 +169,22 @@ def decode(self, notation: str) -> Any: # Handle case where format() creates output like (obj_0) which parser wraps # The parser returns a wrapper Link with no ID, containing the actual Link as first value - if (not link.id and link.values and len(link.values) == 1 and - hasattr(link.values[0], 'id') and link.values[0].id and - link.values[0].id.startswith('obj_')): + if ( + not link.id + and link.values + and len(link.values) == 1 + and hasattr(link.values[0], "id") + and link.values[0].id + and link.values[0].id.startswith("obj_") + ): # Extract the actual Link link = link.values[0] return self._decode_link(link) - def _encode_value(self, obj: Any, visited: Optional[Set[int]] = None, depth: int = 0) -> Link: + def _encode_value( + self, obj: Any, visited: Optional[Set[int]] = None, depth: int = 0 + ) -> Link: """ Encode a value into a Link. @@ -242,7 +249,7 @@ def _encode_value(self, obj: Any, visited: Optional[Set[int]] = None, depth: int elif isinstance(obj, str): # Encode strings as base64 to handle special characters, newlines, etc. - b64_encoded = base64.b64encode(obj.encode('utf-8')).decode('ascii') + b64_encoded = base64.b64encode(obj.encode("utf-8")).decode("ascii") return self._make_link(self.TYPE_STR, b64_encoded) elif isinstance(obj, list): @@ -256,7 +263,9 @@ def _encode_value(self, obj: Any, visited: Optional[Set[int]] = None, depth: int if obj_id in self._encode_memo: ref_id = self._encode_memo[obj_id] # Create the definition with self-reference ID - definition = Link(link_id=ref_id, values=[Link(link_id=self.TYPE_LIST)] + parts) + definition = Link( + link_id=ref_id, values=[Link(link_id=self.TYPE_LIST)] + parts + ) # Store for multi-link output if not at top level if depth > 0: self._all_definitions.append((ref_id, definition)) @@ -281,7 +290,9 @@ def _encode_value(self, obj: Any, visited: Optional[Set[int]] = None, depth: int if obj_id in self._encode_memo: ref_id = self._encode_memo[obj_id] # Create the definition with self-reference ID - definition = Link(link_id=ref_id, values=[Link(link_id=self.TYPE_DICT)] + parts) + definition = Link( + link_id=ref_id, values=[Link(link_id=self.TYPE_DICT)] + parts + ) # Store for multi-link output if not at top level if depth > 0: self._all_definitions.append((ref_id, definition)) @@ -318,10 +329,10 @@ def _decode_link(self, link: Link) -> Any: return self._decode_memo[link.id] # If it starts with obj_, check if we have a forward reference in _all_links - if link.id.startswith('obj_') and self._all_links: + if link.id.startswith("obj_") and self._all_links: # Look for this ID in the remaining links for other_link in self._all_links: - if hasattr(other_link, 'id') and other_link.id == link.id: + if hasattr(other_link, "id") and other_link.id == link.id: # Found it! Decode it now return self._decode_link(other_link) @@ -336,7 +347,7 @@ def _decode_link(self, link: Link) -> Any: # Check if this link has a self-reference ID (format: obj_0: type ...) self_ref_id = None - if link.id and link.id.startswith('obj_'): + if link.id and link.id.startswith("obj_"): self_ref_id = link.id # If this is a back-reference (already in memo), return it if self_ref_id in self._decode_memo: @@ -344,7 +355,7 @@ def _decode_link(self, link: Link) -> Any: # Get the type marker from the first value first_value = link.values[0] - if not hasattr(first_value, 'id') or not first_value.id: + if not hasattr(first_value, "id") or not first_value.id: # Not a type marker we recognize return None @@ -356,21 +367,21 @@ def _decode_link(self, link: Link) -> Any: elif type_marker == self.TYPE_BOOL: if len(link.values) > 1: bool_value = link.values[1] - if hasattr(bool_value, 'id'): + if hasattr(bool_value, "id"): return bool_value.id == "True" return False elif type_marker == self.TYPE_INT: if len(link.values) > 1: int_value = link.values[1] - if hasattr(int_value, 'id'): + if hasattr(int_value, "id"): return int(int_value.id) return 0 elif type_marker == self.TYPE_FLOAT: if len(link.values) > 1: float_value = link.values[1] - if hasattr(float_value, 'id'): + if hasattr(float_value, "id"): value_str = float_value.id if value_str == "NaN": return math.nan @@ -385,12 +396,12 @@ def _decode_link(self, link: Link) -> Any: elif type_marker == self.TYPE_STR: if len(link.values) > 1: str_value = link.values[1] - if hasattr(str_value, 'id'): + if hasattr(str_value, "id"): b64_str = str_value.id # Decode from base64 try: decoded_bytes = base64.b64decode(b64_str) - return decoded_bytes.decode('utf-8') + return decoded_bytes.decode("utf-8") except Exception: # If decode fails, return the raw value return b64_str @@ -420,7 +431,7 @@ def _decode_link(self, link: Link) -> Any: self._decode_memo[dict_id] = result_dict for pair_link in link.values[start_idx:]: - if hasattr(pair_link, 'values') and len(pair_link.values) >= 2: + if hasattr(pair_link, "values") and len(pair_link.values) >= 2: key_link = pair_link.values[0] value_link = pair_link.values[1] diff --git a/python/tests/test_basic_types.py b/python/tests/test_basic_types.py index a88c926..71366f5 100644 --- a/python/tests/test_basic_types.py +++ b/python/tests/test_basic_types.py @@ -131,6 +131,7 @@ def test_roundtrip_float(self): def test_float_special_values(self): """Test encoding/decoding special float values.""" import math + # Test infinity inf_encoded = encode(math.inf) assert decode(inf_encoded) == math.inf diff --git a/python/tests/test_circular_references.py b/python/tests/test_circular_references.py index c774d10..b29d7af 100644 --- a/python/tests/test_circular_references.py +++ b/python/tests/test_circular_references.py @@ -47,7 +47,9 @@ def test_mutual_reference_lists(self): encoded = encode(list1) # Multi-link format is used to avoid parser bug with nested self-references - expected = "(obj_0: list (int 1) (int 2) obj_1)\n(obj_1: list (int 3) (int 4) obj_0)" + expected = ( + "(obj_0: list (int 1) (int 2) obj_1)\n(obj_1: list (int 3) (int 4) obj_0)" + ) assert encoded == expected decoded = decode(encoded)