From fe97566eddbeaccaa7e8861e42f9415aef2c2f16 Mon Sep 17 00:00:00 2001 From: Dak Washbrook Date: Mon, 13 Oct 2025 22:48:46 -0700 Subject: [PATCH 1/5] Add configuration files and update dependencies for project setup --- .changeset/config.json | 10 +- .changeset/fix-env-file-write.md | 10 - .changeset/major-release.md | 25 + .github/workflows/release.yml | 58 +- .node-version | 1 + .npmrc | 1 + .nvmrc | 1 + .prettierignore | 8 + .prettierrc | 23 + .tool-versions | 1 + CONTRIBUTING.md | 103 +++ bun.lock | 328 ++++++++-- eslint.config.ts | 119 ++++ eslint/no-any-except-in-generics.ts | 58 ++ package.json | 49 +- src/cli-write.test.ts | 356 ----------- src/cli.test.ts | 285 ++++++++- src/cli.ts | 335 +--------- ...vironment-variable-value-from-line.test.ts | 126 ---- ...ct-environment-variable-value-from-line.ts | 39 -- ...t-environment-variables-from-file-lines.ts | 18 - src/extract-key-value-from-string.test.ts | 228 ------- src/extract-key-value-from-string.ts | 17 - src/index.test.ts | 182 ++++++ src/index.ts | 202 ++++++ src/integration.test.ts | 595 ------------------ src/regex.test.ts | 80 --- src/regex.ts | 1 - tsconfig.json | 2 +- 29 files changed, 1398 insertions(+), 1863 deletions(-) delete mode 100644 .changeset/fix-env-file-write.md create mode 100644 .changeset/major-release.md create mode 100644 .node-version create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .tool-versions create mode 100644 CONTRIBUTING.md create mode 100644 eslint.config.ts create mode 100644 eslint/no-any-except-in-generics.ts delete mode 100644 src/cli-write.test.ts delete mode 100644 src/extract-environment-variable-value-from-line.test.ts delete mode 100644 src/extract-environment-variable-value-from-line.ts delete mode 100644 src/extract-environment-variables-from-file-lines.ts delete mode 100644 src/extract-key-value-from-string.test.ts delete mode 100644 src/extract-key-value-from-string.ts create mode 100644 src/index.test.ts create mode 100644 src/index.ts delete mode 100644 src/integration.test.ts delete mode 100644 src/regex.test.ts delete mode 100644 src/regex.ts diff --git a/.changeset/config.json b/.changeset/config.json index e37519d..05e2b6c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,13 @@ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": "@changesets/cli/changelog", + "changelog": [ + "@svitejs/changesets-changelog-github-compact", + { "repo": "dak-engineering/synv" } + ], "commit": false, - "fixed": [], - "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "fixed": [], + "linked": [] } \ No newline at end of file diff --git a/.changeset/fix-env-file-write.md b/.changeset/fix-env-file-write.md deleted file mode 100644 index 9694770..0000000 --- a/.changeset/fix-env-file-write.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"synv": patch ---- - -Fix: .env file not being written after synchronization - -The CLI was building the new environment file content but not actually writing it to disk. This fix ensures that: -- The synchronized content is properly written to the .env file -- Additional variables from the existing .env that weren't in .env.example are preserved -- A success message is displayed after successful update diff --git a/.changeset/major-release.md b/.changeset/major-release.md new file mode 100644 index 0000000..e6967fa --- /dev/null +++ b/.changeset/major-release.md @@ -0,0 +1,25 @@ +--- +"synv": major +--- + +Complete rewrite of synv - a tool to sync .env files with .env.example templates. + +## Breaking Changes +- Completely new implementation and behavior +- Now requires Node.js 18+ (ESM modules) + +## New Features +- πŸ”„ Interactive conflict resolution when values differ between .env and .env.example +- πŸ“ Preserves formatting and comments from .env.example +- πŸ”€ Automatically moves stray environment variables to the bottom in alphabetical order +- 🎨 Beautiful CLI with loading spinners and colored output +- πŸ“ Support for custom file paths with -i and -o flags +- πŸ€– CI mode support (set CI=true for non-interactive mode) +- ✨ Prompts for empty values when needed +- πŸ§ͺ Comprehensive test coverage including end-to-end CLI tests + +## How it works +- .env.example is the source of truth for all environment variable keys +- Merges existing values from .env file when available +- Interactive prompts to resolve conflicts (defaults to keeping current .env values) +- Never modifies .env.example file \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a52bf6..99e6d76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,20 +2,24 @@ name: Release on: push: - branches: - - main + branches: [main, alpha, beta, rc] + repository_dispatch: + types: [release] -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: write + id-token: write + pull-requests: write jobs: release: name: Release + if: github.repository_owner == 'dak-engineering' runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - id-token: write - steps: - name: Checkout uses: actions/checkout@v4 @@ -25,30 +29,52 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.0 - name: Install dependencies run: bun install --frozen-lockfile + - name: Build package + run: bun run build + - name: Run tests run: bun test - - name: Build package - run: bun run build + - name: Check for Changesets marked as major + id: major + run: | + echo "found=false" >> $GITHUB_OUTPUT + regex="(major)" + shopt -s nullglob + for file in .changeset/*.md; do + if [[ $(cat $file) =~ $regex ]]; then + echo "found=true" >> $GITHUB_OUTPUT + fi + done - - name: Create Release Pull Request or Publish to npm + - name: Run Changesets (version or publish) id: changesets uses: changesets/action@v1 with: - publish: bun changeset publish - version: bun changeset version - commit: "chore: release package" - title: "chore: release package" + version: bun run changeset:version + publish: bun run changeset:publish + commit: 'ci: Version Packages' + title: 'ci: Version Packages' createGithubReleases: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Auto-merge Changesets PR + if: steps.changesets.outputs.hasChangesets == 'true' && steps.major.outputs.found == 'false' + run: | + gh pr merge --squash "$PR_NUMBER" + gh api --method POST /repos/$REPO/dispatches -f 'event_type=release' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }} + - name: Publish Summary if: steps.changesets.outputs.published == 'true' run: | diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..e222811 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.19.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..449691b --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..3a6161c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.19.0 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d2c7452 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +# Ignore artifacts: +build +coverage + +page.mdx +*.md + +components/shadcn/* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4b15aea --- /dev/null +++ b/.prettierrc @@ -0,0 +1,23 @@ +{ + "experimentalTernaries": false, + "experimentalOperatorPosition": "start", + "singleAttributePerLine": true, + "useTabs": true, + "tabWidth": 2, + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all", + "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], + "importOrder": [ + "^(react/(.*)$)|^(react$)", + "^(next/(.*)$)|^(next$)", + "", + "", + "^~/", + "", + "^@/(.*)$", + "^[./]" + ], + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], + "semi": false +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..a19ca0e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 22.19.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9d7098e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing to synv + +Thank you for your interest in contributing to synv! This document provides guidelines for contributing to the project. + +## Development Setup + +1. Fork and clone this repository +2. Install dependencies: + ```bash + bun install + ``` +3. Run tests: + ```bash + bun test + ``` +4. Build the project: + ```bash + bun run build + ``` + +## Making Changes + +### Creating a Changeset + +We use [changesets](https://github.com/changesets/changesets) to manage versions and changelogs. When you make a change that should be released, you need to create a changeset: + +1. Run the changeset command: + ```bash + bun run changeset + ``` + +2. Select the packages you want to include in the changeset (press space to select) + +3. Choose the type of change: + - **patch**: Bug fixes and minor updates (0.0.X) + - **minor**: New features that are backward compatible (0.X.0) + - **major**: Breaking changes (X.0.0) + +4. Write a brief description of the change. This will appear in the changelog. + +Example changeset file (`.changeset/fluffy-pandas-dance.md`): +```markdown +--- +"synv": patch +--- + +Fixed environment variable parsing for quoted values +``` + +### Commit Messages + +We follow conventional commit messages: +- `fix:` for bug fixes +- `feat:` for new features +- `docs:` for documentation changes +- `chore:` for maintenance tasks +- `ci:` for CI/CD changes + +## Pull Request Process + +1. Create a new branch for your changes +2. Make your changes and add tests if applicable +3. Create a changeset (see above) +4. Commit your changes including the changeset file +5. Push your branch and open a pull request +6. Wait for the CI checks to pass + +## Release Process + +Our release process is fully automated: + +1. When PRs with changesets are merged to `main`, a "Version Packages" PR is automatically created +2. This PR updates package versions and changelogs based on the changesets +3. For non-major releases, the PR is automatically merged +4. For major releases, manual approval is required +5. After merging, packages are automatically published to npm + +### Pre-release versions + +You can also create pre-release versions by pushing to these branches: +- `alpha` - for alpha releases +- `beta` - for beta releases +- `rc` - for release candidates + +## Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test --watch + +# Run tests with coverage +bun run test:coverage + +# Open test UI +bun run test:ui +``` + +## Questions? + +If you have questions, please open an issue on GitHub. \ No newline at end of file diff --git a/bun.lock b/bun.lock index 79cbb5c..596ea14 100644 --- a/bun.lock +++ b/bun.lock @@ -4,28 +4,48 @@ "": { "name": "synv", "dependencies": { - "@inquirer/prompts": "^7.5.0", - "chalk": "^5.4.1", - "cmd-ts": "^0.13.0", - "fuzzbunny": "^1.0.1", - "ora": "^8.2.0", - "remeda": "^2.21.3", + "@inquirer/prompts": "7.8.6", + "chalk": "5.6.2", + "cmd-ts": "0.14.2", + "fuzzbunny": "1.0.1", + "ora": "9.0.0", + "remeda": "2.32.0", }, "devDependencies": { - "@changesets/cli": "^2.29.2", - "@types/node": "^22.15.2", - "@vitest/coverage-v8": "^3.2.4", - "@vitest/ui": "^3.2.4", - "happy-dom": "^20.0.0", - "tsx": "^4.19.3", - "typescript": "^5.8.3", - "vitest": "^3.1.2", + "@changesets/cli": "2.29.7", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.37.0", + "@ianvs/prettier-plugin-sort-imports": "4.7.0", + "@svitejs/changesets-changelog-github-compact": "1.2.0", + "@types/node": "24.7.2", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@vitest/coverage-v8": "3.2.4", + "@vitest/ui": "3.2.4", + "eslint": "9.37.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.4", + "happy-dom": "20.0.0", + "jiti": "2.6.1", + "prettier": "3.6.2", + "prettier-plugin-tailwindcss": "0.6.14", + "tsx": "4.20.6", + "typescript": "5.9.3", + "typescript-eslint": "8.46.1", + "vitest": "3.2.4", }, }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], @@ -34,6 +54,10 @@ "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -52,6 +76,8 @@ "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + "@changesets/get-github-info": ["@changesets/get-github-info@0.6.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA=="], + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.13", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.5", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg=="], "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], @@ -124,6 +150,34 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], + + "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "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.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.7.0", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@prettier/plugin-oxc": "^0.0.4", "@vue/compiler-sfc": "2.7.x || 3.x", "content-tag": "^4.0.0", "prettier": "2 || 3 || ^4.0.0-0", "prettier-plugin-ember-template-tag": "^2.1.0" }, "optionalPeers": ["@prettier/plugin-oxc", "@vue/compiler-sfc", "content-tag", "prettier-plugin-ember-template-tag"] }, "sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.0", "", {}, "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA=="], "@inquirer/checkbox": ["@inquirer/checkbox@4.2.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2n9Vgf4HSciFq8ttKXk+qy+GsyTXPV1An6QAwe/8bkbbqvG4VW1I/ZY1pNu2rf+h9bdzMLPbRSfcNxkHBy/Ydw=="], @@ -180,6 +234,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], @@ -226,16 +282,40 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + "@svitejs/changesets-changelog-github-compact": ["@svitejs/changesets-changelog-github-compact@1.2.0", "", { "dependencies": { "@changesets/get-github-info": "^0.6.0", "dotenv": "^16.0.3" } }, "sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/utils": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -254,13 +334,19 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "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" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], @@ -272,12 +358,14 @@ "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -290,31 +378,39 @@ "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="], "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "cmd-ts": ["cmd-ts@0.13.0", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "didyoumean": "^1.2.2", "strip-ansi": "^6.0.0" } }, "sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g=="], + "cmd-ts": ["cmd-ts@0.14.2", "", { "dependencies": { "chalk": "^5.4.1", "debug": "^4.4.1", "didyoumean": "^1.2.2", "strip-ansi": "^7.1.0" } }, "sha512-YzSXosVEyWVK1IheMKvm6ACejWoSPeKJ18rmXESmMigbShNiQ/rjRifdzeAtKYkuF3Bpf4QjMsBvsjM3UUBwAw=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -322,25 +418,59 @@ "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "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" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], + + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "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" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], @@ -358,12 +488,16 @@ "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "globby": ["globby@11.1.0", "", { "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" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + "happy-dom": ["happy-dom@20.0.0", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-GkWnwIFxVGCf2raNrxImLo397RdGhLapj5cT3R2PT7FwL62Ze1DROhzmYW7+J3p9105DYMVenEejEbnq5wA37w=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -376,6 +510,10 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -404,17 +542,33 @@ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], @@ -432,7 +586,7 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -446,9 +600,15 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "optionator": ["optionator@0.9.4", "", { "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" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="], "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], @@ -456,7 +616,7 @@ "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], @@ -466,6 +626,8 @@ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -486,7 +648,15 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -536,20 +706,24 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], @@ -570,78 +744,140 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "typescript-eslint": ["typescript-eslint@8.46.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.1", "@typescript-eslint/parser": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/utils": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "cmd-ts/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "happy-dom/@types/node": ["@types/node@20.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA=="], - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "ora/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@changesets/parse/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], } } diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..b2458c6 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,119 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import eslintConfigPrettier from 'eslint-config-prettier' +import eslintPluginPrettier from 'eslint-plugin-prettier' +import { defineConfig, globalIgnores } from 'eslint/config' +import tseslint from 'typescript-eslint' + +import { noAnyExceptInGenerics } from './eslint/no-any-except-in-generics' + +// ----------------------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------------------- +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// ----------------------------------------------------------------------------------------- +// Export the flat config array using the `typescript-eslint` helper. This automatically +// wires up the correct parser/plugin and exposes the `strictTypeChecked` preset. +// ----------------------------------------------------------------------------------------- +export default defineConfig( + globalIgnores(['node_modules/**', '**/dist', '**/*.js']), + + // Base JavaScript rules. + js.configs.recommended, + + // TypeScript rules - start with the recommended set and then enable the strict + // type-checked variant which performs full program-level analysis. + ...tseslint.configs.recommended, + ...tseslint.configs.strictTypeChecked, + + // Disable strict type checking for custom ESLint rules + { + files: ['eslint/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + }, + }, + + // Provide project-aware parsing so that the strict presets have full type info. + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: __dirname, + }, + }, + }, + + // Prettier plugin + our opinionated overrides. + { + plugins: { + prettier: eslintPluginPrettier, + }, + rules: { + 'prettier/prettier': 'error', + + 'newline-before-return': 'error', + + // ---------------------------------------------------------------------- + // Existing project-specific rule tweaks + // ---------------------------------------------------------------------- + camelcase: 'off', + 'import/prefer-default-export': 'off', + + // Align with previous configuration - soften a few rules that are too strict + 'no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false, + }, + ], + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + + // TypeScript-specific relaxations + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + + eslintConfigPrettier, + + // Custom configs + defineConfig({ + files: ['src/**/*.ts'], + ignores: ['src/**/*.dts.ts'], + plugins: { + '@nuances': { + rules: { + 'no-any-except-in-generics': noAnyExceptInGenerics, + }, + }, + }, + rules: { + '@nuances/no-any-except-in-generics': 'error', + }, + }), +) diff --git a/eslint/no-any-except-in-generics.ts b/eslint/no-any-except-in-generics.ts new file mode 100644 index 0000000..52dd5c3 --- /dev/null +++ b/eslint/no-any-except-in-generics.ts @@ -0,0 +1,58 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types' +import type { Rule } from 'eslint' + +export const noAnyExceptInGenerics: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: + 'Disallow `any` except when used inside generics (type arguments or type parameter defaults/constraints).', + }, + messages: { + noAny: + 'Unexpected `any`. Only allowed inside generics (e.g., `Foo`, ``, or ``).', + }, + schema: [], + }, + create(context) { + function isAllowedGenericContext(node: TSESTree.TSAnyKeyword): boolean { + let cur = node.parent as TSESTree.Node | undefined + + while (cur) { + const nodeType = cur.type + + // Allowed when `any` is used as a generic type argument: Foo + if (nodeType === AST_NODE_TYPES.TSTypeParameterInstantiation) return true + + // Allowed inside a generic type parameter declaration + // e.g. function f() or interface X {} + if (nodeType === AST_NODE_TYPES.TSTypeParameter) return true + + // Stop climbing once we exit the type position where "generic-ness" would be relevant + // If we hit a function decl, variable decl, or top-level program without finding an allowed ancestor, bail. + if ( + nodeType === AST_NODE_TYPES.Program + || nodeType === AST_NODE_TYPES.VariableDeclarator + || nodeType === AST_NODE_TYPES.TSTypeAnnotation + || nodeType === AST_NODE_TYPES.FunctionDeclaration + || nodeType === AST_NODE_TYPES.ArrowFunctionExpression + || nodeType === AST_NODE_TYPES.FunctionExpression + ) { + break + } + + cur = cur.parent + } + + return false + } + + return { + TSAnyKeyword(node) { + if (!isAllowedGenericContext(node)) { + context.report({ node, messageId: 'noAny' }) + } + }, + } + }, +} diff --git a/package.json b/package.json index 1dc63f8..629db08 100644 --- a/package.json +++ b/package.json @@ -27,29 +27,48 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsx src/cli.ts", + "lint": "eslint src", + "lint:fix": "eslint src --fix", "test": "vitest", + "test:ci": "vitest run", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "changeset": "changeset", - "release": "changeset version && npm run build && changeset publish" + "changeset:version": "changeset version && bun install --lockfile-only", + "changeset:publish": "changeset publish", + "release": "bun run build && bun run changeset:publish" }, "packageManager": "bun@1.3.0", "devDependencies": { - "@changesets/cli": "^2.29.2", - "@types/node": "^22.15.2", - "@vitest/coverage-v8": "^3.2.4", - "@vitest/ui": "^3.2.4", - "happy-dom": "^20.0.0", - "tsx": "^4.19.3", - "typescript": "^5.8.3", - "vitest": "^3.1.2" + "@changesets/cli": "2.29.7", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.37.0", + "@ianvs/prettier-plugin-sort-imports": "4.7.0", + "@svitejs/changesets-changelog-github-compact": "1.2.0", + "@types/node": "24.7.2", + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@vitest/coverage-v8": "3.2.4", + "@vitest/ui": "3.2.4", + "eslint": "9.37.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.4", + "happy-dom": "20.0.0", + "jiti": "2.6.1", + "prettier": "3.6.2", + "prettier-plugin-tailwindcss": "0.6.14", + "tsx": "4.20.6", + "typescript": "5.9.3", + "typescript-eslint": "8.46.1", + "vitest": "3.2.4" }, "dependencies": { - "@inquirer/prompts": "^7.5.0", - "chalk": "^5.4.1", - "cmd-ts": "^0.13.0", - "fuzzbunny": "^1.0.1", - "ora": "^8.2.0", - "remeda": "^2.21.3" + "@inquirer/prompts": "7.8.6", + "chalk": "5.6.2", + "cmd-ts": "0.14.2", + "fuzzbunny": "1.0.1", + "ora": "9.0.0", + "remeda": "2.32.0" } } \ No newline at end of file diff --git a/src/cli-write.test.ts b/src/cli-write.test.ts deleted file mode 100644 index 36ba863..0000000 --- a/src/cli-write.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import fs from 'node:fs/promises' -import path from 'node:path' -import os from 'node:os' -import extractEnvironmentVariablesFromFileLines from './extract-environment-variables-from-file-lines.js' -import extractKeyValueFromString from './extract-key-value-from-string.js' - -describe('CLI - File Writing Logic', () => { - let tempDir: string - - beforeEach(async () => { - // Create a temporary directory for testing - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synv-write-test-')) - }) - - afterEach(async () => { - // Clean up temp directory - await fs.rm(tempDir, { recursive: true, force: true }) - }) - - // Simulate the core logic of the CLI without the interactive parts - async function syncEnvFiles( - envExamplePath: string, - envPath: string, - options: { - skipBackup?: boolean - choices?: Record - customValues?: Record - newValues?: Record - } = {} - ) { - // Read files - const envExampleContent = await fs.readFile(envExamplePath, 'utf8') - const envExampleLines = envExampleContent.split('\n') - - let envLines: string[] = [] - try { - const envContent = await fs.readFile(envPath, 'utf8') - envLines = envContent.split('\n') - } catch { - // File doesn't exist, create empty - await fs.writeFile(envPath, '', 'utf8') - } - - // Create backup if needed - if (!options.skipBackup && envLines.length > 0) { - await fs.copyFile(envPath, `${envPath}.backup`) - } - - // Extract variables - const envFileVariables = extractEnvironmentVariablesFromFileLines(envLines) - const envExampleFileVariables = extractEnvironmentVariablesFromFileLines(envExampleLines) - - // Build new content - const newEnvLines: string[] = [] - const processedKeys = new Set() - - for (const line of envExampleLines) { - const trimmedLine = line.trim() - - if (trimmedLine === '' || trimmedLine.startsWith('#')) { - newEnvLines.push(line) - continue - } - - const exampleEnvVar = extractKeyValueFromString(line) - if (!exampleEnvVar) { - newEnvLines.push(line) - continue - } - - processedKeys.add(exampleEnvVar.key) - - const currentValue = exampleEnvVar.key in envFileVariables && envFileVariables[exampleEnvVar.key] - - if (currentValue !== false) { - // Variable exists in current .env - if (exampleEnvVar.value === '' || currentValue === exampleEnvVar.value) { - // Values match or example is empty - keep current - newEnvLines.push(`${exampleEnvVar.key}="${currentValue}"`) - } else { - // Values differ - use choice from options - const choice = options.choices?.[exampleEnvVar.key] || 'current' - - switch (choice) { - case 'example': - newEnvLines.push(`${exampleEnvVar.key}="${exampleEnvVar.value}"`) - break - case 'custom': - const customValue = options.customValues?.[exampleEnvVar.key] || currentValue - newEnvLines.push(`${exampleEnvVar.key}="${customValue}"`) - break - case 'current': - default: - newEnvLines.push(`${exampleEnvVar.key}="${currentValue}"`) - break - } - } - } else { - // Variable doesn't exist - use new value or example - const newValue = options.newValues?.[exampleEnvVar.key] || exampleEnvVar.value || '' - newEnvLines.push(`${exampleEnvVar.key}="${newValue}"`) - } - } - - // Add any variables from existing .env that weren't in .env.example - for (const [key, value] of Object.entries(envFileVariables)) { - if (!processedKeys.has(key)) { - newEnvLines.push(`${key}="${value}"`) - } - } - - // Write the new content - const newEnvContent = newEnvLines.join('\n') - await fs.writeFile(envPath, newEnvContent, 'utf8') - - return newEnvContent - } - - describe('Creating and editing .env file', () => { - it('should create .env file when it does not exist', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create .env.example - const envExampleContent = `# Test Configuration -NODE_ENV=development -PORT=3000 -API_KEY= -DATABASE_URL=postgresql://localhost:5432/testdb -FEATURE_FLAG=true` - - await fs.writeFile(envExamplePath, envExampleContent) - - // Sync files - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true, - newValues: { - API_KEY: 'test-api-key' - } - }) - - // Verify .env was created - const envExists = await fs.access(envPath).then(() => true).catch(() => false) - expect(envExists).toBe(true) - - // Verify content - expect(result).toContain('NODE_ENV="development"') - expect(result).toContain('PORT="3000"') - expect(result).toContain('API_KEY="test-api-key"') - expect(result).toContain('DATABASE_URL="postgresql://localhost:5432/testdb"') - expect(result).toContain('FEATURE_FLAG="true"') - }) - - it('should edit existing .env file and preserve additional variables', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create .env.example - await fs.writeFile(envExamplePath, `NODE_ENV=production -PORT=8080 -API_KEY= -NEW_VAR=default-value`) - - // Create existing .env - await fs.writeFile(envPath, `NODE_ENV=development -PORT=3000 -API_KEY=existing-key -EXTRA_VAR=should-be-preserved -ANOTHER_EXTRA=also-preserved`) - - // Sync files - keep all current values - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true, - choices: { - NODE_ENV: 'current', - PORT: 'current' - }, - newValues: { - NEW_VAR: 'default-value' - } - }) - - // Verify content - expect(result).toContain('NODE_ENV="development"') - expect(result).toContain('PORT="3000"') - expect(result).toContain('API_KEY="existing-key"') - expect(result).toContain('NEW_VAR="default-value"') - expect(result).toContain('EXTRA_VAR="should-be-preserved"') - expect(result).toContain('ANOTHER_EXTRA="also-preserved"') - }) - - it('should create backup file when not skipped', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - const backupPath = `${envPath}.backup` - - // Create files - await fs.writeFile(envExamplePath, 'NODE_ENV=production') - const originalContent = 'NODE_ENV=development\nAPI_KEY=old-key' - await fs.writeFile(envPath, originalContent) - - // Sync without skipping backup - await syncEnvFiles(envExamplePath, envPath, { - skipBackup: false - }) - - // Verify backup was created - const backupExists = await fs.access(backupPath).then(() => true).catch(() => false) - expect(backupExists).toBe(true) - - // Verify backup content - const backupContent = await fs.readFile(backupPath, 'utf8') - expect(backupContent).toBe(originalContent) - }) - - it('should handle empty .env.example gracefully', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create empty .env.example - await fs.writeFile(envExamplePath, '') - - // Sync files - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true - }) - - // Should create empty .env - const envExists = await fs.access(envPath).then(() => true).catch(() => false) - expect(envExists).toBe(true) - expect(result).toBe('') - }) - - it('should preserve comments and empty lines', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create .env.example with comments - const envExampleContent = `# Application Settings -NODE_ENV=production - -# Database -DATABASE_URL= - -# This is a comment -API_KEY=secret` - - await fs.writeFile(envExamplePath, envExampleContent) - - // Sync files - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true, - newValues: { - DATABASE_URL: 'postgres://localhost' - } - }) - - // Verify structure is preserved - const lines = result.split('\n') - expect(lines[0]).toBe('# Application Settings') - expect(lines[1]).toBe('NODE_ENV="production"') - expect(lines[2]).toBe('') - expect(lines[3]).toBe('# Database') - expect(lines[4]).toBe('DATABASE_URL="postgres://localhost"') - expect(lines[5]).toBe('') - expect(lines[6]).toBe('# This is a comment') - expect(lines[7]).toBe('API_KEY="secret"') - }) - - it('should use example values when specified', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create files - await fs.writeFile(envExamplePath, `NODE_ENV=production -PORT=8080`) - await fs.writeFile(envPath, `NODE_ENV=development -PORT=3000`) - - // Sync with "use example" choices - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true, - choices: { - NODE_ENV: 'example', - PORT: 'example' - } - }) - - // Should use example values - expect(result).toContain('NODE_ENV="production"') - expect(result).toContain('PORT="8080"') - }) - - it('should handle custom values correctly', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create files - await fs.writeFile(envExamplePath, 'DATABASE_URL=postgresql://localhost:5432/mydb') - await fs.writeFile(envPath, 'DATABASE_URL=postgresql://localhost:5432/olddb') - - // Sync with custom value - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true, - choices: { - DATABASE_URL: 'custom' - }, - customValues: { - DATABASE_URL: 'postgresql://remote:5432/newdb' - } - }) - - // Should use custom value - expect(result).toBe('DATABASE_URL="postgresql://remote:5432/newdb"') - }) - - it('should handle variables with special characters', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create .env.example with special characters - await fs.writeFile(envExamplePath, `API_KEY=sk_test_123 -CONNECTION_STRING="Server=localhost;Database=mydb;User Id=sa;Password=P@ssw0rd!"`) - - // Sync files - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true - }) - - // Verify special characters are preserved - expect(result).toContain('API_KEY="sk_test_123"') - expect(result).toContain('CONNECTION_STRING="Server=localhost;Database=mydb;User Id=sa;Password=P@ssw0rd!"') - }) - - it('should handle single line values correctly', async () => { - const envExamplePath = path.join(tempDir, '.env.example') - const envPath = path.join(tempDir, '.env') - - // Create files with various value formats - await fs.writeFile(envExamplePath, `SINGLE_LINE=value -QUOTED_VALUE="already quoted" -EMPTY_VALUE=`) - - // Sync files - const result = await syncEnvFiles(envExamplePath, envPath, { - skipBackup: true - }) - - // Verify content - expect(result).toContain('SINGLE_LINE="value"') - expect(result).toContain('QUOTED_VALUE="already quoted"') - expect(result).toContain('EMPTY_VALUE=""') - }) - }) -}) diff --git a/src/cli.test.ts b/src/cli.test.ts index 0519ecb..39bacfa 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1 +1,284 @@ - \ No newline at end of file +import { execSync } from 'child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('CLI End-to-End Tests', () => { + let tempDir: string + const cliPath = join(process.cwd(), 'dist', 'cli.js') + + beforeEach(() => { + // Create a temporary directory for test files + tempDir = mkdtempSync(join(tmpdir(), 'synv-test-')) + }) + + afterEach(() => { + // Clean up temporary directory + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('Basic functionality', () => { + it('should display help when --help flag is used', () => { + let output = '' + try { + output = execSync(`node ${cliPath} --help`, { encoding: 'utf-8' }) + } catch (error: unknown) { + // cmd-ts exits with code 1 for help, capture stdout/stderr + const execError = error as { stdout?: string; stderr?: string } + output = execError.stdout || execError.stderr || '' + } + + expect(output).toContain('synv') + expect(output).toContain('Sync your .env file with .env.example') + expect(output).toContain('--env-example-file') + expect(output).toContain('--env-file') + expect(output).toContain('-i') + expect(output).toContain('-o') + }) + + it('should display version when --version flag is used', () => { + const output = execSync(`node ${cliPath} --version`, { encoding: 'utf-8' }) + + expect(output.trim()).toBe('0.1.5') + }) + }) + + describe('File synchronization', () => { + it('should sync .env with .env.example using default paths', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + // Create test files + writeFileSync( + envExamplePath, + `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" + +# Database +DB_URL=""`, + ) + + writeFileSync( + envPath, + `NEXT_PUBLIC_APP_HOST="http://production.com" +DB_URL="postgres://localhost"`, + ) + + // Run CLI in non-interactive mode (using environment variable) + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + const result = readFileSync(envPath, 'utf-8') + + expect(result).toContain('# App') + expect(result).toContain('NEXT_PUBLIC_APP_HOST="http://production.com"') + expect(result).toContain('# Database') + expect(result).toContain('DB_URL="postgres://localhost"') + }) + + it('should work with custom file paths using -i and -o flags', () => { + const customExamplePath = join(tempDir, '.env.template') + const customEnvPath = join(tempDir, '.env.local') + + writeFileSync(customExamplePath, `API_KEY=""`) + writeFileSync(customEnvPath, `API_KEY="secret123"`) + + execSync(`node ${cliPath} -i .env.template -o .env.local`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + const result = readFileSync(customEnvPath, 'utf-8') + expect(result.trim()).toBe('API_KEY="secret123"') + }) + + it('should create .env file if it does not exist', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + writeFileSync(envExamplePath, `NEW_VAR="default"`) + + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + expect(existsSync(envPath)).toBe(true) + const result = readFileSync(envPath, 'utf-8') + expect(result.trim()).toBe('NEW_VAR="default"') + }) + + it('should handle stray variables by moving them to the bottom', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + writeFileSync( + envExamplePath, + `# Main vars +MAIN_VAR="value"`, + ) + + writeFileSync( + envPath, + `MAIN_VAR="value" +STRAY_B="b" +STRAY_A="a"`, + ) + + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + const result = readFileSync(envPath, 'utf-8') + + expect(result).toContain('# Main vars') + expect(result).toContain('MAIN_VAR="value"') + expect(result).toContain('# Additional environment variables') + + // Check alphabetical order + const lines = result.split('\n') + const strayAIndex = lines.findIndex((l) => l.includes('STRAY_A')) + const strayBIndex = lines.findIndex((l) => l.includes('STRAY_B')) + expect(strayAIndex).toBeLessThan(strayBIndex) + }) + }) + + describe('Error handling', () => { + it('should fail gracefully when .env.example does not exist', () => { + try { + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + expect.fail('Should have thrown an error') + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string } + expect(execError.stderr || execError.stdout).toContain('.env.example file not found') + } + }) + + it('should fail with custom example file path that does not exist', () => { + try { + execSync(`node ${cliPath} -i .env.nonexistent`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + expect.fail('Should have thrown an error') + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string } + expect(execError.stderr || execError.stdout).toContain('.env.nonexistent file not found') + } + }) + }) + + describe('Complex scenarios', () => { + it('should preserve comment structure and spacing', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + writeFileSync( + envExamplePath, + `# Section 1 +VAR1="value1" + +# Section 2 +VAR2="value2" +VAR3="value3" + +# Section 3 +VAR4="value4"`, + ) + + writeFileSync( + envPath, + `VAR1="custom1" +VAR2="custom2" +VAR3="custom3" +VAR4="custom4"`, + ) + + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + const result = readFileSync(envPath, 'utf-8') + + // Check that comments and spacing are preserved + expect(result).toBe(`# Section 1 +VAR1="custom1" +# Section 2 +VAR2="custom2" +VAR3="custom3" + +# Section 3 +VAR4="custom4"`) + }) + + it('should handle empty values correctly', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + writeFileSync( + envExamplePath, + `FILLED="" +EMPTY=""`, + ) + + writeFileSync(envPath, `FILLED="has value"`) + + execSync(`node ${cliPath}`, { + cwd: tempDir, + encoding: 'utf-8', + env: { ...process.env, CI: 'true' }, + }) + + const result = readFileSync(envPath, 'utf-8') + expect(result).toBe(`FILLED="has value" +EMPTY=""`) + }) + }) +}) + +describe('CLI Interactive Tests', () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'synv-interactive-test-')) + }) + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + it('should handle interactive prompts (simulated)', () => { + const envExamplePath = join(tempDir, '.env.example') + const envPath = join(tempDir, '.env') + + writeFileSync(envExamplePath, `CONFLICT_VAR="example_value"`) + writeFileSync(envPath, `CONFLICT_VAR="current_value"`) + + // Note: In a real scenario, we would need to use a library like 'node-pty' + // or 'execa' with stdin simulation to test interactive prompts. + // For now, we're testing that the interactive mode would be triggered. + + // This test confirms the setup is correct for interactive mode + expect(existsSync(envExamplePath)).toBe(true) + expect(existsSync(envPath)).toBe(true) + }) +}) diff --git a/src/cli.ts b/src/cli.ts index bd04f6f..adc47a8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,324 +1,39 @@ #!/usr/bin/env node -import { command, run, string, option, flag, boolean } from 'cmd-ts' -import { search, confirm, select, input } from '@inquirer/prompts' import chalk from 'chalk' -import fs from 'node:fs/promises' -import fuzz from 'fuzzbunny' -import path from 'node:path' -import ora from 'ora' -import extractEnvironmentVariablesFromFileLines from './extract-environment-variables-from-file-lines.js' -import extractKeyValueFromString from './extract-key-value-from-string.js' +import { command, option, optional, run, string } from 'cmd-ts' -const { bold, green, red } = chalk +import syncEnvFiles from './index.js' -const cwdFiles = await fs.readdir('.') - -const syncCommand = command({ +const app = command({ name: 'synv', - description: 'Synchronise your stuff', + description: 'Sync your .env file with .env.example', + version: '0.1.5', args: { - envExampleFilePath: option({ - type: string, + input: option({ + short: 'i', long: 'env-example-file', - short: 'x', - description: 'Relative path to .env.example file', - defaultValue: () => '.env.example', + type: optional(string), + description: 'Path to the .env.example file (default: .env.example)', }), - envFilePath: option({ - type: string, + output: option({ + short: 'o', long: 'env-file', - short: 'e', - description: 'Relative path to .env file', - defaultValue: () => '.env', - }), - skipBackup: flag({ - long: 'skip-backup', - description: 'Skip backing up the .env file', - type: boolean, - defaultValue: () => { - return false - }, - }), - quiet: flag({ - long: 'quiet', - description: 'Suppress output', - type: boolean, - defaultValue: () => { - return false - }, + type: optional(string), + description: 'Path to the .env file (default: .env)', }), }, - async handler(inputs) { - const log = (...messages: string[]) => { - if (!inputs.quiet) { - console.log(...messages) - } - } - - const envExampleFile = await getFileContentsByLine({ - relativeFilePath: inputs.envExampleFilePath, - files: cwdFiles, - defaultFileName: '.env.example', - }) - - const envFile = await getFileContentsByLine({ - relativeFilePath: inputs.envFilePath, - files: cwdFiles, - defaultFileName: '.env', - createIfNotExists: true, - }) - - if (!inputs.skipBackup) { - await backupFile(envFile.path) - } - - const envFileVariables = extractEnvironmentVariablesFromFileLines(envFile.lines) - const envExampleFileVariables = extractEnvironmentVariablesFromFileLines(envExampleFile.lines) - - const newEnvLines: string[] = [] - const processedKeys = new Set() - - for (const line of envExampleFile.lines) { - const trimmedLine = line.trim() - - if (trimmedLine === '' || trimmedLine.startsWith('#')) { - newEnvLines.push(line) - continue - } - - const exampleEnvVar = extractKeyValueFromString(line) - if (!exampleEnvVar) { - newEnvLines.push(line) - continue - } - - processedKeys.add(exampleEnvVar.key) - - const currentValue = envFileVariables[exampleEnvVar.key] - const hasCurrentValue = currentValue !== undefined - - if (hasCurrentValue) { - const valuesMatch = exampleEnvVar.value === '' || currentValue === exampleEnvVar.value - - if (valuesMatch) { - log(`Keeping ${bold(exampleEnvVar.key)}`) - newEnvLines.push(`${exampleEnvVar.key}="${currentValue}"`) - } else { - const choice = await select({ - message: `${exampleEnvVar.key} differs`, - choices: [ - { name: 'Keep current', value: 'current' }, - { name: 'Use example', value: 'example' }, - { name: 'Enter new', value: 'custom' }, - ], - }) - - switch (choice) { - case 'example': - newEnvLines.push(`${exampleEnvVar.key}="${exampleEnvVar.value}"`) - break - case 'current': - newEnvLines.push(`${exampleEnvVar.key}="${currentValue}"`) - break - case 'custom': - const newValue = await input({ - message: `Enter value for ${exampleEnvVar.key}`, - }) - newEnvLines.push(`${exampleEnvVar.key}="${newValue}"`) - break - default: - newEnvLines.push(`${exampleEnvVar.key}="${currentValue}"`) - break - } - } - } else { - const newValue = await input({ - message: `Enter value for ${exampleEnvVar.key}`, - default: exampleEnvVar.value || undefined, - }) - newEnvLines.push(`${exampleEnvVar.key}="${newValue}"`) - } - } - - // Add any variables from existing .env that weren't in .env.example - for (const [key, value] of Object.entries(envFileVariables)) { - if (!processedKeys.has(key)) { - log(`Preserving additional variable ${bold(key)}`) - newEnvLines.push(`${key}="${value}"`) - } - } - - // Write the new content to the .env file - const newEnvContent = newEnvLines.join('\n') - await fs.writeFile(envFile.path, newEnvContent, 'utf8') - log(green(`βœ“ Successfully updated ${bold(path.basename(envFile.path))}`)) - } -}) - -const backupFile = async (filepath: string) => { - await loadWhile('Backing up .env file', async () => { - await fs.copyFile(filepath, `${filepath}.backup`) - }, { - successMessage: `Backup of ${bold(path.basename(filepath))} complete`, - }) -} - -const getFileContentsByLine = async ({ - relativeFilePath, - files, - defaultFileName, - createIfNotExists = false, -}: { - relativeFilePath?: string - files: string[] - defaultFileName?: string - createIfNotExists?: boolean -}) => { - const filePath = (relativeFilePath && path.join(process.cwd(), relativeFilePath)) ?? - (defaultFileName && await loadWhile(`Attempting to auto-detect ${bold(defaultFileName)} file path`, async () => { - return await autoDetectFilePath(files, defaultFileName) - }, { - successMessage: `Found ${defaultFileName} file`, - throwOnError: false, - })) ?? - await promptForFilePath(files, 'Select your .env.example file') - - const fileExists = await checkFileExists(filePath) - - if (!fileExists && !createIfNotExists) { - console.error(red('File not found:'), filePath) - process.exit(1) - } else if (!fileExists && createIfNotExists) { - await fs.writeFile(filePath, '') - } - - return { - path: filePath, - lines: await getFileLinesInArray(filePath), - } -} - -type LoadWhileOptions = { - successMessage?: string | ((result: T) => string) - failureMessage?: string | ((error: Error) => string) - throwOnError?: boolean -} - -const loadWhile = async ( - message: string, - callback: () => T, - options: LoadWhileOptions = {} -): Promise => { - const { successMessage, failureMessage, throwOnError = true } = options - const spinner = ora(message).start() - - try { - const result = await callback() - - const finalMessage = typeof successMessage === 'function' - ? successMessage(result) - : successMessage ?? message - - spinner.succeed(finalMessage) - return result - } catch (error) { - const errorObj = error instanceof Error ? error : new Error('Unknown error has occurred.') - - if (failureMessage) { - const finalFailureMessage = typeof failureMessage === 'function' - ? failureMessage(errorObj) - : failureMessage - spinner.fail(finalFailureMessage) - } else { - spinner.fail(errorObj.message) - } - - if (throwOnError) { - throw error - } - - return null - } -} - -const getFileLinesInArray = async (filepath: string) => { - const fileContents = await fs.readFile(filepath, 'utf8') - - return fileContents.split('\n') -} - -const checkFileExists = async (filepath: string) => { - try { - await fs.access(filepath) - - return true - } catch { - return false - } -} - -const promptForFilePath = async (files: string[], message: string) => { - return search({ - message, - source: async (input) => { - if (!input) { - return files.map((name, value) => { - return { - name, - value: path.join(process.cwd(), name), - } - }) - } - - return highlightFuzzyMatches(cwdFiles, input) - } - }) -} - -const highlightFuzzyMatches = (files: string[], input: string) => { - const fileItems = files.map(filename => ({ filename })) - - const matches = fuzz.fuzzyFilter(fileItems, input, { - fields: ['filename'], - }) - - const highlightedMatches = matches.map((match) => { - if (!match.highlights.filename) { - return match.item.filename - } - - return match.highlights.filename - .map((namePiece, index) => { - // Even indices are non-matching parts, odd indices are matches - return index % 2 === 0 ? namePiece : green(bold(namePiece)) + handler: async ({ input, output }) => { + try { + await syncEnvFiles({ + envExampleFile: input, + envFile: output, + interactive: !process.env.CI, }) - .join('') - }) - - return highlightedMatches.map((name) => ({ - name, - value: path.join(process.cwd(), name), - })) -} - -const autoDetectFilePath = async (files: string[], filenameToDetect: string) => { - const hasEnvExampleFile = files.includes(filenameToDetect) - - if (hasEnvExampleFile) { - const confirmed = await promptToConfirmEnvExampleFile() - - if (confirmed) { - return path.join(process.cwd(), filenameToDetect) + } catch (error) { + console.error(chalk.red('Error:'), error instanceof Error ? error.message : error) + process.exit(1) } - } - - throw new Error(`Could not auto-detect ${filenameToDetect}`) -} - -const promptToConfirmEnvExampleFile = () => { - return confirm({ - message: 'Use this file? (.env.example)', - }) -} + }, +}) -void run(syncCommand, process.argv.slice(2)); +void run(app, process.argv.slice(2)) diff --git a/src/extract-environment-variable-value-from-line.test.ts b/src/extract-environment-variable-value-from-line.test.ts deleted file mode 100644 index 66584b7..0000000 --- a/src/extract-environment-variable-value-from-line.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { describe, it, expect } from 'vitest' -import extractEnvironmentVariableValueFromLine from './extract-environment-variable-value-from-line.js' - -describe('extractEnvironmentVariableValueFromLine', () => { - describe('basic values', () => { - it('should extract simple unquoted values', () => { - expect(extractEnvironmentVariableValueFromLine('value')).toBe('value') - expect(extractEnvironmentVariableValueFromLine('123')).toBe('123') - expect(extractEnvironmentVariableValueFromLine('true')).toBe('true') - expect(extractEnvironmentVariableValueFromLine('localhost:3000')).toBe('localhost:3000') - }) - - it('should handle empty values', () => { - expect(extractEnvironmentVariableValueFromLine('')).toBe('') - expect(extractEnvironmentVariableValueFromLine(' ')).toBe('') - }) - - it('should trim whitespace from unquoted values', () => { - expect(extractEnvironmentVariableValueFromLine(' value ')).toBe('value') - expect(extractEnvironmentVariableValueFromLine('\tvalue\t')).toBe('value') - }) - }) - - describe('quoted values', () => { - it('should extract double-quoted values', () => { - expect(extractEnvironmentVariableValueFromLine('"quoted value"')).toBe('quoted value') - expect(extractEnvironmentVariableValueFromLine('"value with spaces"')).toBe('value with spaces') - expect(extractEnvironmentVariableValueFromLine('""')).toBe('') - }) - - it('should extract single-quoted values', () => { - expect(extractEnvironmentVariableValueFromLine("'quoted value'")).toBe('quoted value') - expect(extractEnvironmentVariableValueFromLine("'value with spaces'")).toBe('value with spaces') - expect(extractEnvironmentVariableValueFromLine("''")).toBe('') - }) - - it('should preserve spaces inside quotes', () => { - expect(extractEnvironmentVariableValueFromLine('" spaced "')).toBe(' spaced ') - expect(extractEnvironmentVariableValueFromLine("' spaced '")).toBe(' spaced ') - }) - - it('should handle quotes within different quote types', () => { - expect(extractEnvironmentVariableValueFromLine('"value with \'single\' quotes"')).toBe("value with 'single' quotes") - expect(extractEnvironmentVariableValueFromLine("'value with \"double\" quotes'")).toBe('value with "double" quotes') - }) - - it('should handle unclosed quotes by including them in the value', () => { - expect(extractEnvironmentVariableValueFromLine('"unclosed')).toBe('"unclosed') - expect(extractEnvironmentVariableValueFromLine("'unclosed")).toBe("'unclosed") - }) - }) - - describe('comments handling', () => { - it('should stop at # when not inside quotes', () => { - expect(extractEnvironmentVariableValueFromLine('value#comment')).toBe('value') - expect(extractEnvironmentVariableValueFromLine('value # comment')).toBe('value') - expect(extractEnvironmentVariableValueFromLine('value # comment')).toBe('value') - }) - - it('should preserve # inside quoted values', () => { - expect(extractEnvironmentVariableValueFromLine('"value#hash"')).toBe('value#hash') - expect(extractEnvironmentVariableValueFromLine("'value#hash'")).toBe('value#hash') - expect(extractEnvironmentVariableValueFromLine('"#hashtag"')).toBe('#hashtag') - }) - - it('should handle # after closing quotes', () => { - expect(extractEnvironmentVariableValueFromLine('"value"#comment')).toBe('value') - expect(extractEnvironmentVariableValueFromLine("'value'#comment")).toBe('value') - expect(extractEnvironmentVariableValueFromLine('"value" #comment')).toBe('value') - }) - - it('should handle just a comment', () => { - expect(extractEnvironmentVariableValueFromLine('#comment')).toBe('') - expect(extractEnvironmentVariableValueFromLine(' #comment')).toBe('') - }) - }) - - describe('special characters and edge cases', () => { - it('should handle URLs', () => { - expect(extractEnvironmentVariableValueFromLine('https://example.com')).toBe('https://example.com') - expect(extractEnvironmentVariableValueFromLine('"https://example.com/path?query=1"')).toBe('https://example.com/path?query=1') - }) - - it('should handle database connection strings', () => { - const dbUrl = 'postgresql://user:pass@localhost:5432/dbname' - expect(extractEnvironmentVariableValueFromLine(dbUrl)).toBe(dbUrl) - expect(extractEnvironmentVariableValueFromLine(`"${dbUrl}"`)).toBe(dbUrl) - }) - - it('should handle JSON strings', () => { - const json = '{"key":"value"}' - expect(extractEnvironmentVariableValueFromLine(`'${json}'`)).toBe(json) - }) - - it('should handle escaped quotes (though not specially processed)', () => { - // Note: The current implementation doesn't handle escaped quotes specially - // This test documents the current behavior - expect(extractEnvironmentVariableValueFromLine('\\"value\\"')).toBe('\\"value\\"') - expect(extractEnvironmentVariableValueFromLine('"\\"nested\\""')).toBe('\\"nested\\"') - }) - - it('should handle values with equals signs', () => { - expect(extractEnvironmentVariableValueFromLine('key=value')).toBe('key=value') - expect(extractEnvironmentVariableValueFromLine('"key=value"')).toBe('key=value') - }) - - it('should handle environment variable references', () => { - expect(extractEnvironmentVariableValueFromLine('${OTHER_VAR}')).toBe('${OTHER_VAR}') - expect(extractEnvironmentVariableValueFromLine('"${OTHER_VAR}/path"')).toBe('${OTHER_VAR}/path') - }) - - it('should handle multiline-like values (though .env files are typically single-line)', () => { - expect(extractEnvironmentVariableValueFromLine('line1\\nline2')).toBe('line1\\nline2') - expect(extractEnvironmentVariableValueFromLine('"line1\\nline2"')).toBe('line1\\nline2') - }) - }) - - describe('whitespace edge cases', () => { - it('should handle various whitespace combinations', () => { - expect(extractEnvironmentVariableValueFromLine(' "value" ')).toBe('value') - expect(extractEnvironmentVariableValueFromLine(" 'value' ")).toBe('value') - expect(extractEnvironmentVariableValueFromLine(' value #comment')).toBe('value') - expect(extractEnvironmentVariableValueFromLine('\t\tvalue\t\t')).toBe('value') - }) - }) -}) diff --git a/src/extract-environment-variable-value-from-line.ts b/src/extract-environment-variable-value-from-line.ts deleted file mode 100644 index a5ed083..0000000 --- a/src/extract-environment-variable-value-from-line.ts +++ /dev/null @@ -1,39 +0,0 @@ -const extractEnvironmentVariableValueFromLine = (value: string) => { - let result = '' - let insideQuotes = false - let quoteChar = '' - - for (let charPosition = 0; charPosition < value.length; charPosition++) { - const char = value[charPosition] - const isQuote = char === '"' || char === '\'' - const isEscaped = charPosition > 0 && value[charPosition - 1] === '\\' - - if (isQuote && !isEscaped) { - if (!insideQuotes) { - insideQuotes = true - quoteChar = char - } else if (char === quoteChar) { - insideQuotes = false - quoteChar = '' - } - } - - if (char === '#' && !insideQuotes) { - break - } - - result += char - } - - result = result.trim() - const isQuoted = (result.startsWith('\'') && result.endsWith('\'')) || - (result.startsWith('"') && result.endsWith('"')) - - if (isQuoted) { - result = result.slice(1, -1) - } - - return result -} - -export default extractEnvironmentVariableValueFromLine diff --git a/src/extract-environment-variables-from-file-lines.ts b/src/extract-environment-variables-from-file-lines.ts deleted file mode 100644 index 20d164b..0000000 --- a/src/extract-environment-variables-from-file-lines.ts +++ /dev/null @@ -1,18 +0,0 @@ -import extractEnvironmentVariableValueFromLine from './extract-environment-variable-value-from-line.js' -import { ENV_VAR_REGEX } from './regex.js' - -const extractEnvironmentVariablesFromFileLines = (lines: string[]) => { - const environmentVariables: Record = {} - - for (const line of lines) { - const match = line.match(ENV_VAR_REGEX) - - if (match) { - environmentVariables[match[1]] = extractEnvironmentVariableValueFromLine(match[2]) - } - } - - return environmentVariables -} - -export default extractEnvironmentVariablesFromFileLines diff --git a/src/extract-key-value-from-string.test.ts b/src/extract-key-value-from-string.test.ts deleted file mode 100644 index 99f6fce..0000000 --- a/src/extract-key-value-from-string.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, it, expect } from 'vitest' -import extractKeyValueFromString from './extract-key-value-from-string.js' - -describe('extractKeyValueFromString', () => { - describe('valid environment variable lines', () => { - it('should extract key and value from simple declarations', () => { - expect(extractKeyValueFromString('KEY=value')).toEqual({ - key: 'KEY', - value: 'value' - }) - - expect(extractKeyValueFromString('DATABASE_URL=localhost')).toEqual({ - key: 'DATABASE_URL', - value: 'localhost' - }) - - expect(extractKeyValueFromString('PORT=3000')).toEqual({ - key: 'PORT', - value: '3000' - }) - }) - - it('should handle empty values', () => { - expect(extractKeyValueFromString('EMPTY=')).toEqual({ - key: 'EMPTY', - value: '' - }) - - expect(extractKeyValueFromString('ANOTHER_EMPTY=""')).toEqual({ - key: 'ANOTHER_EMPTY', - value: '' - }) - }) - - it('should handle quoted values', () => { - expect(extractKeyValueFromString('QUOTED="value with spaces"')).toEqual({ - key: 'QUOTED', - value: 'value with spaces' - }) - - expect(extractKeyValueFromString("SINGLE_QUOTED='value with spaces'")).toEqual({ - key: 'SINGLE_QUOTED', - value: 'value with spaces' - }) - }) - - it('should handle values with special characters', () => { - expect(extractKeyValueFromString('URL=https://example.com:8080/path?query=1')).toEqual({ - key: 'URL', - value: 'https://example.com:8080/path?query=1' - }) - - expect(extractKeyValueFromString('CONNECTION=user:pass@host')).toEqual({ - key: 'CONNECTION', - value: 'user:pass@host' - }) - - expect(extractKeyValueFromString('PATH=/usr/local/bin:/usr/bin')).toEqual({ - key: 'PATH', - value: '/usr/local/bin:/usr/bin' - }) - }) - - it('should handle values with equals signs', () => { - expect(extractKeyValueFromString('EQUATION=a=b+c')).toEqual({ - key: 'EQUATION', - value: 'a=b+c' - }) - - expect(extractKeyValueFromString('MULTIPLE_EQUALS=key=value=another')).toEqual({ - key: 'MULTIPLE_EQUALS', - value: 'key=value=another' - }) - }) - - it('should handle comments in values', () => { - expect(extractKeyValueFromString('KEY=value#comment')).toEqual({ - key: 'KEY', - value: 'value' - }) - - expect(extractKeyValueFromString('KEY=value # comment')).toEqual({ - key: 'KEY', - value: 'value' - }) - - expect(extractKeyValueFromString('KEY="value#notacomment"')).toEqual({ - key: 'KEY', - value: 'value#notacomment' - }) - }) - - it('should handle environment variable references', () => { - expect(extractKeyValueFromString('KEY=${OTHER_VAR}')).toEqual({ - key: 'KEY', - value: '${OTHER_VAR}' - }) - - expect(extractKeyValueFromString('PATH=${HOME}/bin:${PATH}')).toEqual({ - key: 'PATH', - value: '${HOME}/bin:${PATH}' - }) - }) - - it('should handle keys with numbers and underscores', () => { - expect(extractKeyValueFromString('KEY_123=value')).toEqual({ - key: 'KEY_123', - value: 'value' - }) - - expect(extractKeyValueFromString('MY_2ND_KEY=value')).toEqual({ - key: 'MY_2ND_KEY', - value: 'value' - }) - - expect(extractKeyValueFromString('KEY123=value')).toEqual({ - key: 'KEY123', - value: 'value' - }) - }) - - it('should handle whitespace in values correctly', () => { - expect(extractKeyValueFromString('KEY= value ')).toEqual({ - key: 'KEY', - value: 'value' - }) - - expect(extractKeyValueFromString('KEY=" value "')).toEqual({ - key: 'KEY', - value: ' value ' - }) - - expect(extractKeyValueFromString('KEY=\tvalue\t')).toEqual({ - key: 'KEY', - value: 'value' - }) - }) - }) - - describe('invalid environment variable lines', () => { - it('should return null for lines without equals sign', () => { - expect(extractKeyValueFromString('KEY')).toBeNull() - expect(extractKeyValueFromString('JUST_A_KEY')).toBeNull() - }) - - it('should return null for lines without a key', () => { - expect(extractKeyValueFromString('=value')).toBeNull() - expect(extractKeyValueFromString(' =value')).toBeNull() - }) - - it('should return null for comment lines', () => { - expect(extractKeyValueFromString('#KEY=value')).toBeNull() - expect(extractKeyValueFromString('# This is a comment')).toBeNull() - expect(extractKeyValueFromString(' # KEY=value')).toBeNull() - }) - - it('should return null for empty lines', () => { - expect(extractKeyValueFromString('')).toBeNull() - expect(extractKeyValueFromString(' ')).toBeNull() - expect(extractKeyValueFromString('\t\t')).toBeNull() - }) - - it('should return null for lines with invalid key names', () => { - expect(extractKeyValueFromString('key-with-dash=value')).toBeNull() - expect(extractKeyValueFromString('key.with.dot=value')).toBeNull() - expect(extractKeyValueFromString('123KEY=value')).toBeNull() - expect(extractKeyValueFromString('KEY WITH SPACE=value')).toBeNull() - }) - - it('should return null for lines with spaces before equals', () => { - expect(extractKeyValueFromString('KEY =value')).toBeNull() - expect(extractKeyValueFromString('KEY =value')).toBeNull() - }) - - it('should return null for malformed lines', () => { - expect(extractKeyValueFromString('not an env var')).toBeNull() - expect(extractKeyValueFromString('this is just text')).toBeNull() - expect(extractKeyValueFromString('KEY: value')).toBeNull() - }) - }) - - describe('edge cases', () => { - it('should handle very long values', () => { - const longValue = 'a'.repeat(1000) - expect(extractKeyValueFromString(`KEY=${longValue}`)).toEqual({ - key: 'KEY', - value: longValue - }) - }) - - it('should handle complex real-world examples', () => { - const dbUrl = 'postgresql://user:password@localhost:5432/dbname?sslmode=require' - expect(extractKeyValueFromString(`DATABASE_URL="${dbUrl}"`)).toEqual({ - key: 'DATABASE_URL', - value: dbUrl - }) - - const jsonConfig = '{"key":"value","nested":{"prop":123}}' - expect(extractKeyValueFromString(`CONFIG='${jsonConfig}'`)).toEqual({ - key: 'CONFIG', - value: jsonConfig - }) - - const multiPath = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' - expect(extractKeyValueFromString(`PATH=${multiPath}`)).toEqual({ - key: 'PATH', - value: multiPath - }) - }) - - it('should handle Base64 encoded values', () => { - const base64 = 'SGVsbG8gV29ybGQhCg==' - expect(extractKeyValueFromString(`SECRET_KEY=${base64}`)).toEqual({ - key: 'SECRET_KEY', - value: base64 - }) - }) - - it('should handle values with line continuations (backslash)', () => { - // Note: .env files typically don't support line continuations, - // but the value might contain backslashes - expect(extractKeyValueFromString('KEY=value\\ncontinued')).toEqual({ - key: 'KEY', - value: 'value\\ncontinued' - }) - }) - }) -}) diff --git a/src/extract-key-value-from-string.ts b/src/extract-key-value-from-string.ts deleted file mode 100644 index 9b3ee07..0000000 --- a/src/extract-key-value-from-string.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ENV_VAR_REGEX } from './regex.js' -import extractEnvironmentVariableValueFromLine from './extract-environment-variable-value-from-line.js' - -const extractKeyValueFromString = (str: string) => { - const match = str.match(ENV_VAR_REGEX) - - if (!match) { - return null - } - - return { - key: match[1], - value: extractEnvironmentVariableValueFromLine(match[2]), - } -} - -export default extractKeyValueFromString diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..d8cd0bd --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,182 @@ +import * as fs from 'fs' +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest' + +import { syncEnvFiles } from './index' + +vi.mock('fs', () => ({ + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + existsSync: vi.fn(), +})) + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), +})) + +describe('syncEnvFiles', () => { + const mockedReadFileSync = fs.readFileSync as Mock + const mockedWriteFileSync = fs.writeFileSync as Mock + const mockedExistsSync = fs.existsSync as Mock + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should sync .env with .env.example format and content', async () => { + const envExampleContent = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" + +# CMS +PAYLOAD_SECRET="abc123" +DRAFT_MODE_SECRET="abc123" + +# Turso +TURSO_DATABASE_URL="libsql://localhost:8080" +TURSO_AUTH_TOKEN=""` + + const envContent = `NEXT_PUBLIC_APP_HOST="http://localhost:3000" +PAYLOAD_SECRET="abc123" +DRAFT_MODE_SECRET="abc123" +TURSO_DATABASE_URL="libsql://localhost:8080" +TURSO_AUTH_TOKEN="1234"` + + mockedExistsSync.mockImplementation((path) => { + return path === '.env.example' || path === '.env' + }) + + mockedReadFileSync.mockImplementation((path) => { + if (path === '.env.example') return envExampleContent + if (path === '.env') return envContent + throw new Error('File not found') + }) + + await syncEnvFiles({ interactive: false }) + + const expectedOutput = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" +# CMS +PAYLOAD_SECRET="abc123" +DRAFT_MODE_SECRET="abc123" + +# Turso +TURSO_DATABASE_URL="libsql://localhost:8080" +TURSO_AUTH_TOKEN="1234"` + + expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + }) + + it('should handle stray environment variables', async () => { + const envExampleContent = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000"` + + const envContent = `NEXT_PUBLIC_APP_HOST="http://localhost:3000" +STRAY_VAR_B="value_b" +STRAY_VAR_A="value_a" +ANOTHER_VAR="another_value"` + + mockedExistsSync.mockImplementation((path) => { + return path === '.env.example' || path === '.env' + }) + + mockedReadFileSync.mockImplementation((path) => { + if (path === '.env.example') return envExampleContent + if (path === '.env') return envContent + throw new Error('File not found') + }) + + await syncEnvFiles({ interactive: false }) + + const expectedOutput = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" + +# Additional environment variables +ANOTHER_VAR="another_value" +STRAY_VAR_A="value_a" +STRAY_VAR_B="value_b"` + + expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + }) + + it('should create .env file if it does not exist', async () => { + const envExampleContent = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" + +# CMS +PAYLOAD_SECRET="abc123"` + + mockedExistsSync.mockImplementation((path) => { + return path === '.env.example' + }) + + mockedReadFileSync.mockImplementation((path) => { + if (path === '.env.example') return envExampleContent + throw new Error('File not found') + }) + + await syncEnvFiles({ interactive: false }) + + const expectedOutput = `# App +NEXT_PUBLIC_APP_HOST="http://localhost:3000" +# CMS +PAYLOAD_SECRET="abc123"` + + expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + }) + + it('should handle custom file paths', async () => { + const envExampleContent = `KEY="value"` + const envContent = `KEY="different"` + + mockedExistsSync.mockImplementation((path) => { + return path === '.env.custom.example' || path === '.env.custom' + }) + + mockedReadFileSync.mockImplementation((path) => { + if (path === '.env.custom.example') return envExampleContent + if (path === '.env.custom') return envContent + throw new Error('File not found') + }) + + await syncEnvFiles({ + envExampleFile: '.env.custom.example', + envFile: '.env.custom', + interactive: false, + }) + + expect(mockedWriteFileSync).toHaveBeenCalledWith('.env.custom', 'KEY="different"') + }) + + it('should throw error if .env.example does not exist', async () => { + mockedExistsSync.mockReturnValue(false) + + await expect(syncEnvFiles({ interactive: false })).rejects.toThrow( + '.env.example file not found', + ) + }) + + it('should handle empty values from example', async () => { + const envExampleContent = `DATABASE_URL="" +API_KEY=""` + + const envContent = `DATABASE_URL="postgres://localhost" +API_KEY=""` + + mockedExistsSync.mockImplementation((path) => { + return path === '.env.example' || path === '.env' + }) + + mockedReadFileSync.mockImplementation((path) => { + if (path === '.env.example') return envExampleContent + if (path === '.env') return envContent + throw new Error('File not found') + }) + + await syncEnvFiles({ interactive: false }) + + const expectedOutput = `DATABASE_URL="postgres://localhost" +API_KEY=""` + + expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + }) +}) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f17fb5a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,202 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs' +import { input, select } from '@inquirer/prompts' +import chalk from 'chalk' +import ora from 'ora' + +interface ParsedEnv { + entries: Array<{ key: string; value: string; comment?: string }> + raw: string +} + +interface SyncOptions { + envExampleFile?: string + envFile?: string + interactive?: boolean +} + +function parseEnvFile(content: string): ParsedEnv { + const lines = content.split('\n') + const entries: Array<{ key: string; value: string; comment?: string }> = [] + let currentComment: string | undefined + + for (const line of lines) { + const trimmed = line.trim() + + if (trimmed.startsWith('#')) { + currentComment = line + continue + } + + if (trimmed === '') { + currentComment = undefined + continue + } + + const match = line.match(/^([^=]+)=(.*)$/) + if (match) { + const key = match[1].trim() + const value = match[2].trim() + entries.push({ + key, + value, + comment: currentComment, + }) + currentComment = undefined + } + } + + return { entries, raw: content } +} + +function formatEnvEntry(entry: { key: string; value: string; comment?: string }): string { + let result = '' + if (entry.comment) { + result += entry.comment + '\n' + } + result += `${entry.key}=${entry.value}` + + return result +} + +async function resolveValue( + key: string, + exampleValue: string, + currentValue: string | undefined, + interactive: boolean, +): Promise { + if (!interactive) { + return currentValue ?? exampleValue + } + + if (currentValue === undefined) { + if (exampleValue === '""' || exampleValue === "''") { + const userValue = await input({ + message: `Enter value for ${chalk.cyan(key)} (press Enter for empty):`, + default: '', + }) + + return userValue === '' ? '""' : userValue + } + + return exampleValue + } + + if (currentValue === exampleValue) { + return currentValue + } + + if (exampleValue === '""' || exampleValue === "''" || exampleValue === '') { + return currentValue + } + + const choice = await select({ + message: `Conflict for ${chalk.cyan(key)}:`, + choices: [ + { + name: `Keep current: ${chalk.green(currentValue)}`, + value: 'current', + }, + { + name: `Use example: ${chalk.yellow(exampleValue)}`, + value: 'example', + }, + ], + default: 'current', + }) + + return choice === 'current' ? currentValue : exampleValue +} + +export async function syncEnvFiles(options: SyncOptions = {}): Promise { + const envExampleFile = options.envExampleFile || '.env.example' + const envFile = options.envFile || '.env' + const interactive = options.interactive ?? true + + const spinner = interactive ? ora('Reading environment files...').start() : null + + try { + if (!existsSync(envExampleFile)) { + spinner?.fail(`${envExampleFile} file not found`) + throw new Error(`${envExampleFile} file not found`) + } + + const exampleContent = readFileSync(envExampleFile, 'utf-8') + const exampleParsed = parseEnvFile(exampleContent) + + let currentParsed: ParsedEnv = { entries: [], raw: '' } + if (existsSync(envFile)) { + const currentContent = readFileSync(envFile, 'utf-8') + currentParsed = parseEnvFile(currentContent) + } + + spinner?.succeed('Environment files loaded') + + const currentEnvMap = new Map(currentParsed.entries.map((e) => [e.key, e.value])) + + const mergedEntries: Array<{ key: string; value: string; comment?: string }> = [] + const usedKeys = new Set() + + for (const exampleEntry of exampleParsed.entries) { + const currentValue = currentEnvMap.get(exampleEntry.key) + const resolvedValue = await resolveValue( + exampleEntry.key, + exampleEntry.value, + currentValue, + interactive, + ) + + mergedEntries.push({ + ...exampleEntry, + value: resolvedValue, + }) + usedKeys.add(exampleEntry.key) + } + + const strayEntries = currentParsed.entries + .filter((e) => !usedKeys.has(e.key)) + .sort((a, b) => a.key.localeCompare(b.key)) + + const writeSpinner = interactive ? ora('Writing synchronized .env file...').start() : null + + const output: string[] = [] + + for (let i = 0; i < mergedEntries.length; i++) { + const entry = mergedEntries[i] + const prevEntry = i > 0 ? mergedEntries[i - 1] : undefined + + if (entry.comment && prevEntry && !prevEntry.comment) { + output.push('') + } + + output.push(formatEnvEntry(entry)) + } + + if (strayEntries.length > 0) { + output.push('') + output.push('# Additional environment variables') + for (const entry of strayEntries) { + output.push(`${entry.key}=${entry.value}`) + } + } + + const finalContent = output.join('\n') + writeFileSync(envFile, finalContent) + + writeSpinner?.succeed( + chalk.green(`Successfully synced ${chalk.bold(envFile)} with ${chalk.bold(envExampleFile)}`), + ) + + if (strayEntries.length > 0 && interactive) { + console.log( + chalk.yellow( + `\n⚠ ${String(strayEntries.length)} additional variables were moved to the bottom of ${envFile}`, + ), + ) + } + } catch (error) { + spinner?.fail('Failed to sync environment files') + throw error + } +} + +export default syncEnvFiles diff --git a/src/integration.test.ts b/src/integration.test.ts deleted file mode 100644 index 9cbb3bc..0000000 --- a/src/integration.test.ts +++ /dev/null @@ -1,595 +0,0 @@ -import { describe, it, expect } from 'vitest' -import extractEnvironmentVariablesFromFileLines from './extract-environment-variables-from-file-lines.js' -import extractKeyValueFromString from './extract-key-value-from-string.js' -import extractEnvironmentVariableValueFromLine from './extract-environment-variable-value-from-line.js' - -describe('Integration Tests - Full .env Processing', () => { - describe('Complete .env.example to .env transformation', () => { - const envExampleContent = `# Application Configuration -NODE_ENV=production -PORT=3000 - -# Database Configuration -DATABASE_URL=postgresql://user:password@localhost:5432/mydb -DATABASE_POOL_MIN=2 -DATABASE_POOL_MAX=10 -REDIS_URL=redis://localhost:6379 - -# Authentication -JWT_SECRET=your-secret-key-here -JWT_EXPIRES_IN=7d -BCRYPT_ROUNDS=10 - -# Third-party Services -STRIPE_API_KEY=sk_test_... -STRIPE_WEBHOOK_SECRET=whsec_... -SENDGRID_API_KEY= -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_REGION=us-east-1 -AWS_S3_BUCKET=my-app-uploads - -# Feature Flags -ENABLE_ANALYTICS=true -ENABLE_BETA_FEATURES=false -DEBUG_MODE=false - -# URLs and Endpoints -API_BASE_URL=https://api.example.com -FRONTEND_URL=https://example.com -WEBHOOK_ENDPOINT=https://api.example.com/webhooks - -# Monitoring -SENTRY_DSN= -LOG_LEVEL=info -# Options: error, warn, info, debug, trace - -# Email Configuration -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER= -SMTP_PASS= -FROM_EMAIL=noreply@example.com - -# Rate Limiting -RATE_LIMIT_WINDOW=15m -RATE_LIMIT_MAX_REQUESTS=100 - -# File Upload -MAX_FILE_SIZE=10485760 -# 10MB in bytes -ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf - -# Cache Configuration -CACHE_TTL=3600 -# 1 hour in seconds -CACHE_PREFIX=myapp_ - -# Miscellaneous -TIMEZONE=UTC -DEFAULT_LANGUAGE=en -MAINTENANCE_MODE=false - -# Complex Values -JSON_CONFIG={"key":"value","nested":{"prop":123}} -MULTILINE_VALUE="This is a value that could be long" -SPECIAL_CHARS="!@#$%^&*()_+-=[]{}|;:,.<>?" -PATH_VAR=/usr/local/bin:/usr/bin:/bin -BASE64_SECRET=SGVsbG8gV29ybGQhCg== - -# Environment Variables with References -FULL_DATABASE_URL=\${DATABASE_URL}?ssl=true -API_KEY_COMBINED=\${STRIPE_API_KEY}_\${NODE_ENV} - -# Empty values that need to be filled -REQUIRED_SECRET= -OPTIONAL_CONFIG=` - - const existingEnvContent = `# Application Configuration -NODE_ENV=development -PORT=4000 - -# Database Configuration -DATABASE_URL=postgresql://devuser:devpass@localhost:5432/devdb -DATABASE_POOL_MIN=1 -DATABASE_POOL_MAX=5 -REDIS_URL=redis://localhost:6379/1 - -# Authentication -JWT_SECRET=dev-secret-key-12345 -JWT_EXPIRES_IN=30d -BCRYPT_ROUNDS=8 - -# Third-party Services -STRIPE_API_KEY=sk_test_oldkey123 -STRIPE_WEBHOOK_SECRET=whsec_oldwebhook456 -SENDGRID_API_KEY=SG.actualkey789 -AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -AWS_REGION=us-west-2 -AWS_S3_BUCKET=my-dev-bucket - -# Feature Flags -ENABLE_ANALYTICS=false -ENABLE_BETA_FEATURES=true -DEBUG_MODE=true - -# URLs and Endpoints -API_BASE_URL=http://localhost:3000 -FRONTEND_URL=http://localhost:8080 -WEBHOOK_ENDPOINT=http://localhost:3000/webhooks - -# Monitoring -SENTRY_DSN=https://abc123@sentry.io/project -LOG_LEVEL=debug - -# Email Configuration -SMTP_HOST=mailhog -SMTP_PORT=1025 -SMTP_USER=testuser -SMTP_PASS=testpass -FROM_EMAIL=test@localhost - -# Rate Limiting -RATE_LIMIT_WINDOW=1m -RATE_LIMIT_MAX_REQUESTS=1000 - -# File Upload -MAX_FILE_SIZE=52428800 -ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf,text/plain - -# Cache Configuration -CACHE_TTL=60 -CACHE_PREFIX=dev_ - -# Miscellaneous -TIMEZONE=America/New_York -DEFAULT_LANGUAGE=es -MAINTENANCE_MODE=false - -# Complex Values -JSON_CONFIG={"key":"devvalue","nested":{"prop":456}} -MULTILINE_VALUE="Dev environment value" -SPECIAL_CHARS=test123 -PATH_VAR=/home/user/bin:/usr/local/bin:/usr/bin:/bin -BASE64_SECRET=ZGV2ZWxvcG1lbnQ= - -# Custom dev-only variables not in example -DEV_ONLY_VAR=should_be_preserved -ANOTHER_DEV_VAR=keep_this_too` - - it('should extract all variables from .env.example file', () => { - const lines = envExampleContent.split('\n') - const variables = extractEnvironmentVariablesFromFileLines(lines) - - // Check that all non-empty variables are extracted - expect(variables.NODE_ENV).toBe('production') - expect(variables.PORT).toBe('3000') - expect(variables.DATABASE_URL).toBe('postgresql://user:password@localhost:5432/mydb') - expect(variables.JWT_SECRET).toBe('your-secret-key-here') - expect(variables.ENABLE_ANALYTICS).toBe('true') - expect(variables.API_BASE_URL).toBe('https://api.example.com') - expect(variables.LOG_LEVEL).toBe('info') - expect(variables.RATE_LIMIT_WINDOW).toBe('15m') - expect(variables.MAX_FILE_SIZE).toBe('10485760') - expect(variables.CACHE_TTL).toBe('3600') - expect(variables.TIMEZONE).toBe('UTC') - expect(variables.JSON_CONFIG).toBe('{"key":"value","nested":{"prop":123}}') - expect(variables.SPECIAL_CHARS).toBe('!@#$%^&*()_+-=[]{}|;:,.<>?') - expect(variables.BASE64_SECRET).toBe('SGVsbG8gV29ybGQhCg==') - - // Check empty values - expect(variables.SENDGRID_API_KEY).toBe('') - expect(variables.AWS_ACCESS_KEY_ID).toBe('') - expect(variables.SENTRY_DSN).toBe('') - expect(variables.SMTP_USER).toBe('') - expect(variables.REQUIRED_SECRET).toBe('') - expect(variables.OPTIONAL_CONFIG).toBe('') - - // Check that comments are not included - expect(variables['#']).toBeUndefined() - expect(variables['Options:']).toBeUndefined() - }) - - it('should extract all variables from existing .env file', () => { - const lines = existingEnvContent.split('\n') - const variables = extractEnvironmentVariablesFromFileLines(lines) - - // Check existing values - expect(variables.NODE_ENV).toBe('development') - expect(variables.PORT).toBe('4000') - expect(variables.DATABASE_URL).toBe('postgresql://devuser:devpass@localhost:5432/devdb') - expect(variables.JWT_SECRET).toBe('dev-secret-key-12345') - expect(variables.STRIPE_API_KEY).toBe('sk_test_oldkey123') - expect(variables.SENDGRID_API_KEY).toBe('SG.actualkey789') - expect(variables.AWS_ACCESS_KEY_ID).toBe('AKIAIOSFODNN7EXAMPLE') - expect(variables.LOG_LEVEL).toBe('debug') - expect(variables.SMTP_USER).toBe('testuser') - expect(variables.CACHE_TTL).toBe('60') - - // Check dev-only variables - expect(variables.DEV_ONLY_VAR).toBe('should_be_preserved') - expect(variables.ANOTHER_DEV_VAR).toBe('keep_this_too') - }) - - it('should correctly merge .env.example structure with existing .env values', () => { - const exampleLines = envExampleContent.split('\n') - const existingLines = existingEnvContent.split('\n') - - const exampleVars = extractEnvironmentVariablesFromFileLines(exampleLines) - const existingVars = extractEnvironmentVariablesFromFileLines(existingLines) - - const mergedEnv: string[] = [] - const processedKeys = new Set() - - // Process each line from .env.example - for (const line of exampleLines) { - const trimmedLine = line.trim() - - // Keep comments and empty lines - if (trimmedLine === '' || trimmedLine.startsWith('#')) { - mergedEnv.push(line) - continue - } - - const keyValue = extractKeyValueFromString(line) - if (!keyValue) { - mergedEnv.push(line) - continue - } - - processedKeys.add(keyValue.key) - - // Use existing value if available, otherwise use example value - const value = existingVars[keyValue.key] !== undefined - ? existingVars[keyValue.key] - : keyValue.value - - mergedEnv.push(`${keyValue.key}=${value}`) - } - - // Add variables that exist in .env but not in .env.example - mergedEnv.push('') - mergedEnv.push('# Additional variables from existing .env') - for (const [key, value] of Object.entries(existingVars)) { - if (!processedKeys.has(key)) { - mergedEnv.push(`${key}=${value}`) - } - } - - const mergedContent = mergedEnv.join('\n') - - // Verify structure is preserved - expect(mergedContent).toContain('# Application Configuration') - expect(mergedContent).toContain('# Database Configuration') - expect(mergedContent).toContain('# Authentication') - - // Verify existing values are used - expect(mergedContent).toContain('NODE_ENV=development') - expect(mergedContent).toContain('PORT=4000') - expect(mergedContent).toContain('DATABASE_URL=postgresql://devuser:devpass@localhost:5432/devdb') - expect(mergedContent).toContain('JWT_SECRET=dev-secret-key-12345') - expect(mergedContent).toContain('STRIPE_API_KEY=sk_test_oldkey123') - expect(mergedContent).toContain('SENDGRID_API_KEY=SG.actualkey789') - - // Verify dev-only variables are preserved - expect(mergedContent).toContain('DEV_ONLY_VAR=should_be_preserved') - expect(mergedContent).toContain('ANOTHER_DEV_VAR=keep_this_too') - - // Verify empty values in example are filled from existing - expect(mergedContent).toContain('AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE') - expect(mergedContent).toContain('SMTP_USER=testuser') - - // Verify comments are preserved - expect(mergedContent).toContain('# 10MB in bytes') - expect(mergedContent).toContain('# 1 hour in seconds') - expect(mergedContent).toContain('# Options: error, warn, info, debug, trace') - }) - - it('should handle edge cases in environment variable processing', () => { - const edgeCaseContent = `# Edge cases -EMPTY= -EMPTY_QUOTES="" -SINGLE_QUOTES='value' -DOUBLE_QUOTES="value" -WITH_HASH=value#comment -QUOTED_HASH="value#notcomment" -EQUALS_IN_VALUE=key=value=another -SPACES_AROUND= spaced -QUOTED_SPACES=" spaced " -SPECIAL_CHARS="!@#$%^&*()" -URL_WITH_PARAMS=https://api.com?key=value&other=123 -JSON={"complex":{"nested":["array",123,true]}} -ESCAPED_QUOTES="He said \\"Hello\\"" -BACKTICKS=\`command\` -DOLLAR_SIGN=$100 -VARIABLE_REF=\${OTHER_VAR} -MULTIPLE_REFS=\${VAR1}_\${VAR2} -COMMENT_AFTER=value # this is a comment -NO_COMMENT="value # not a comment" -UNICODE=Hello δΈ–η•Œ 🌍 -LONG_VALUE=${'a'.repeat(1000)} -` - - const lines = edgeCaseContent.split('\n') - const variables = extractEnvironmentVariablesFromFileLines(lines) - - // Test empty values - expect(variables.EMPTY).toBe('') - expect(variables.EMPTY_QUOTES).toBe('') - - // Test quoted values - expect(variables.SINGLE_QUOTES).toBe('value') - expect(variables.DOUBLE_QUOTES).toBe('value') - - // Test hash/comment handling - expect(variables.WITH_HASH).toBe('value') - expect(variables.QUOTED_HASH).toBe('value#notcomment') - - // Test special characters - expect(variables.EQUALS_IN_VALUE).toBe('key=value=another') - expect(variables.SPECIAL_CHARS).toBe('!@#$%^&*()') - expect(variables.URL_WITH_PARAMS).toBe('https://api.com?key=value&other=123') - - // Test spacing - expect(variables.SPACES_AROUND).toBe('spaced') - expect(variables.QUOTED_SPACES).toBe(' spaced ') - - // Test complex values - expect(variables.JSON).toBe('{"complex":{"nested":["array",123,true]}}') - expect(variables.VARIABLE_REF).toBe('${OTHER_VAR}') - expect(variables.MULTIPLE_REFS).toBe('${VAR1}_${VAR2}') - - // Test comments - expect(variables.COMMENT_AFTER).toBe('value') - expect(variables.NO_COMMENT).toBe('value # not a comment') - - // Test unicode - expect(variables.UNICODE).toBe('Hello δΈ–η•Œ 🌍') - - // Test long value - expect(variables.LONG_VALUE).toBe('a'.repeat(1000)) - }) - - it('should create a properly formatted merged .env file', () => { - const exampleLines = envExampleContent.split('\n') - const existingLines = existingEnvContent.split('\n') - - const existingVars = extractEnvironmentVariablesFromFileLines(existingLines) - - const mergedEnv: string[] = [] - const processedKeys = new Set() - - // Process .env.example maintaining structure - for (const line of exampleLines) { - const trimmedLine = line.trim() - - if (trimmedLine === '' || trimmedLine.startsWith('#')) { - mergedEnv.push(line) - continue - } - - const keyValue = extractKeyValueFromString(line) - if (!keyValue) { - mergedEnv.push(line) - continue - } - - processedKeys.add(keyValue.key) - const value = existingVars[keyValue.key] !== undefined - ? existingVars[keyValue.key] - : keyValue.value - - // Format the value with quotes if needed - const needsQuotes = value.includes(' ') || value.includes('#') || - value.includes('"') || value.includes("'") || - value.startsWith(' ') || value.endsWith(' ') - - if (needsQuotes && !value.startsWith('"') && !value.startsWith("'")) { - mergedEnv.push(`${keyValue.key}="${value}"`) - } else { - mergedEnv.push(`${keyValue.key}=${value}`) - } - } - - // Add extra variables - const extraVars = Object.entries(existingVars) - .filter(([key]) => !processedKeys.has(key)) - - if (extraVars.length > 0) { - mergedEnv.push('') - mergedEnv.push('# Additional variables from existing .env') - for (const [key, value] of extraVars) { - mergedEnv.push(`${key}=${value}`) - } - } - - const finalContent = mergedEnv.join('\n') - - // Verify the final content is valid - const finalLines = finalContent.split('\n') - const finalVars = extractEnvironmentVariablesFromFileLines(finalLines) - - // All variables from existing .env should be present - for (const [key, value] of Object.entries(existingVars)) { - expect(finalVars[key]).toBe(value) - } - - // Structure from .env.example should be maintained - expect(finalContent.split('\n').filter(l => l.startsWith('#')).length) - .toBeGreaterThan(10) // Should have many comment lines preserved - }) - }) - - describe('Syncing with inline comments and existing values', () => { - it('should preserve existing values and handle inline comments correctly', () => { - const envExampleLines = [ - '# Posthog', - 'NEXT_PUBLIC_POSTHOG_HOST="https://us.posthog.com"', - 'NEXT_PUBLIC_POSTHOG_KEY=""', - '', - '# MyAion', - 'NEXT_PUBLIC_MYAION_SOCKET="https://myaion-socket.up.railway.app/"', - 'MYAION_GQL_HOST="https://api.myaion.eu/graphql"', - 'MYAION_SECRET=""', - '', - '# App', - 'APP_METADATA_BASE_URL="http://localhost:3000"', - 'DATABASE_URL="mysql://root:app@127.0.0.1:3306/app"', - 'FLAGS_SECRET="FAKE_DEFAULT_FLAGS_SECRET_xyz789" # fake secret for dev', - '', - '# Discord', - 'DISCORD_SERVER_ID="607005182915772427"', - 'DISCORD_CLIENT_ID=""', - 'DISCORD_CLIENT_SECRET=""', - 'DISCORD_REDIRECT_URI="http://localhost:3000/api/auth/discord/callback"', - 'DISCORD_BOT_TOKEN=""', - '', - '# Payload', - 'PAYLOAD_SECRET="FAKE_DEFAULT_PAYLOAD_abc123" # fake secret for dev', - '', - '# Auth', - 'AUTH_SECRET="FAKE_DEFAULT_AUTH_SECRET_qwerty123=" # fake secret for dev', - 'NEXTAUTH_URL="http://localhost:3000"' - ] - - const existingEnvLines = [ - '# Posthog', - 'NEXT_PUBLIC_POSTHOG_HOST="https://us.posthog.com"', - 'NEXT_PUBLIC_POSTHOG_KEY="phc_FAKE1234567890abcdefghijklmnopqrstuvwxyz"', - '', - '# MyAion', - 'NEXT_PUBLIC_MYAION_SOCKET="https://myaion-socket.up.railway.app/"', - 'MYAION_GQL_HOST="https://api.myaion.eu/graphql"', - 'MYAION_SECRET="FAKE_SECRET_aGVsbG8td29ybGQtdGhpcy1pcy1ub3QtcmVhbA=="', - '', - '# App', - 'APP_METADATA_BASE_URL="http://localhost:3000"', - 'DATABASE_URL="mysql://root:app@127.0.0.1:3306/app"', - 'FLAGS_SECRET="FAKE_FLAGS_SECRET_1234567890abcdefghijklmn"', - '', - '# Discord', - 'DISCORD_SERVER_ID="607005182915772427"', - 'DISCORD_CLIENT_ID="1234567890123456789"', - 'DISCORD_CLIENT_SECRET="FAKE_DISCORD_SECRET_abcd1234"', - 'DISCORD_REDIRECT_URI="http://localhost:3000/api/auth/discord/callback"', - 'DISCORD_BOT_TOKEN="FAKE.BOT.TOKEN.1234567890abcdefghijklmnopqrstuvwxyz"', - '', - '# Payload', - 'PAYLOAD_SECRET="FAKE_PAYLOAD_0123456789abcdef"', - '', - '# Auth', - '# This is randomly generated not an actual secret that is used anywhere outside of dev', - 'AUTH_SECRET="FAKE_AUTH_SECRET_abc123def456ghi789jkl0mno="', - 'NEXTAUTH_URL="http://localhost:3000"' - ] - - // Extract variables from example file - const exampleVars = extractEnvironmentVariablesFromFileLines(envExampleLines) - - // Extract variables from existing env file - const existingVars = extractEnvironmentVariablesFromFileLines(existingEnvLines) - - // Test that all example variables are found - expect(exampleVars).toHaveProperty('NEXT_PUBLIC_POSTHOG_HOST') - expect(exampleVars).toHaveProperty('NEXT_PUBLIC_POSTHOG_KEY') - expect(exampleVars).toHaveProperty('FLAGS_SECRET') - expect(exampleVars).toHaveProperty('PAYLOAD_SECRET') - expect(exampleVars).toHaveProperty('AUTH_SECRET') - - // Test that existing values are preserved - expect(existingVars.NEXT_PUBLIC_POSTHOG_KEY).toBe('phc_FAKE1234567890abcdefghijklmnopqrstuvwxyz') - expect(existingVars.MYAION_SECRET).toBe('FAKE_SECRET_aGVsbG8td29ybGQtdGhpcy1pcy1ub3QtcmVhbA==') - expect(existingVars.DISCORD_CLIENT_ID).toBe('1234567890123456789') - expect(existingVars.DISCORD_CLIENT_SECRET).toBe('FAKE_DISCORD_SECRET_abcd1234') - expect(existingVars.DISCORD_BOT_TOKEN).toBe('FAKE.BOT.TOKEN.1234567890abcdefghijklmnopqrstuvwxyz') - - // Test that default values from .env.example are correctly extracted - expect(exampleVars.FLAGS_SECRET).toBe('FAKE_DEFAULT_FLAGS_SECRET_xyz789') - expect(exampleVars.PAYLOAD_SECRET).toBe('FAKE_DEFAULT_PAYLOAD_abc123') - expect(exampleVars.AUTH_SECRET).toBe('FAKE_DEFAULT_AUTH_SECRET_qwerty123=') - - // Simulate the sync process - existing values should override example defaults - const syncedVars: Record = {} - - // First, add all variables from example - Object.entries(exampleVars).forEach(([key, value]) => { - syncedVars[key] = value - }) - - // Then override with existing values - Object.entries(existingVars).forEach(([key, value]) => { - if (value !== '') { - syncedVars[key] = value - } - }) - - // Verify synced results preserve existing values - expect(syncedVars.NEXT_PUBLIC_POSTHOG_KEY).toBe('phc_FAKE1234567890abcdefghijklmnopqrstuvwxyz') - expect(syncedVars.FLAGS_SECRET).toBe('FAKE_FLAGS_SECRET_1234567890abcdefghijklmn') - expect(syncedVars.PAYLOAD_SECRET).toBe('FAKE_PAYLOAD_0123456789abcdef') - expect(syncedVars.AUTH_SECRET).toBe('FAKE_AUTH_SECRET_abc123def456ghi789jkl0mno=') - - // Verify empty values from example are filled with existing values - expect(syncedVars.DISCORD_CLIENT_ID).toBe('1234567890123456789') - expect(syncedVars.DISCORD_CLIENT_SECRET).toBe('FAKE_DISCORD_SECRET_abcd1234') - }) - - it('should handle inline comments in environment variables', () => { - const linesWithInlineComments = [ - 'FLAGS_SECRET="FAKE_DEFAULT_FLAGS_SECRET_xyz789" # fake secret for dev', - 'PAYLOAD_SECRET="FAKE_DEFAULT_PAYLOAD_abc123" # fake secret for dev', - 'AUTH_SECRET="FAKE_DEFAULT_AUTH_SECRET_qwerty123=" # fake secret for dev', - 'NORMAL_VAR="value"', - 'COMMENTED_EMPTY="" # this should have a default' - ] - - linesWithInlineComments.forEach(line => { - const result = extractKeyValueFromString(line) - - if (line.includes('FLAGS_SECRET')) { - expect(result).toEqual({ - key: 'FLAGS_SECRET', - value: 'FAKE_DEFAULT_FLAGS_SECRET_xyz789' - }) - } else if (line.includes('PAYLOAD_SECRET')) { - expect(result).toEqual({ - key: 'PAYLOAD_SECRET', - value: 'FAKE_DEFAULT_PAYLOAD_abc123' - }) - } else if (line.includes('AUTH_SECRET')) { - expect(result).toEqual({ - key: 'AUTH_SECRET', - value: 'FAKE_DEFAULT_AUTH_SECRET_qwerty123=' - }) - } else if (line.includes('NORMAL_VAR')) { - expect(result).toEqual({ - key: 'NORMAL_VAR', - value: 'value' - }) - } else if (line.includes('COMMENTED_EMPTY')) { - expect(result).toEqual({ - key: 'COMMENTED_EMPTY', - value: '' - }) - } - }) - }) - - it('should preserve additional comments from existing .env file', () => { - const existingEnvWithExtraComments = [ - '# Auth', - '# This is randomly generated not an actual secret that is used anywhere outside of dev', - 'AUTH_SECRET="FAKE_AUTH_SECRET_abc123def456ghi789jkl0mno="', - 'NEXTAUTH_URL="http://localhost:3000"' - ] - - // The sync process should preserve the additional comment line - const hasAdditionalComment = existingEnvWithExtraComments.some( - line => line.includes('This is randomly generated') - ) - - expect(hasAdditionalComment).toBe(true) - }) - }) -}) diff --git a/src/regex.test.ts b/src/regex.test.ts deleted file mode 100644 index 93840e9..0000000 --- a/src/regex.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { ENV_VAR_REGEX } from './regex.js' - -describe('ENV_VAR_REGEX', () => { - it('should match valid environment variable declarations', () => { - const validCases = [ - 'KEY=value', - 'DATABASE_URL=postgresql://localhost:5432/mydb', - 'API_KEY=abc123', - 'PORT=3000', - 'DEBUG=true', - 'EMPTY_VALUE=', - 'WITH_SPACES=value with spaces', - 'WITH_EQUALS=value=with=equals', - 'NUMBERS123=456', - 'UNDERSCORE_KEY=value', - 'KEY_WITH_123_NUMBERS=value', - ] - - validCases.forEach(testCase => { - const match = testCase.match(ENV_VAR_REGEX) - expect(match).toBeTruthy() - expect(match?.[0]).toBe(testCase) - }) - }) - - it('should capture key and value groups correctly', () => { - const testCases = [ - { input: 'KEY=value', expectedKey: 'KEY', expectedValue: 'value' }, - { input: 'DATABASE_URL=postgresql://localhost', expectedKey: 'DATABASE_URL', expectedValue: 'postgresql://localhost' }, - { input: 'EMPTY=', expectedKey: 'EMPTY', expectedValue: '' }, - { input: 'WITH_SPACES=value with spaces', expectedKey: 'WITH_SPACES', expectedValue: 'value with spaces' }, - ] - - testCases.forEach(({ input, expectedKey, expectedValue }) => { - const match = input.match(ENV_VAR_REGEX) - expect(match?.[1]).toBe(expectedKey) - expect(match?.[2]).toBe(expectedValue) - }) - }) - - it('should not match invalid environment variable declarations', () => { - const invalidCases = [ - '=value', // No key - 'KEY', // No equals sign - ' KEY=value', // Leading space - 'key-with-dash=value', // Dash in key - 'key.with.dot=value', // Dot in key - '123KEY=value', // Starting with number - 'KEY =value', // Space before equals - '#COMMENT=value', // Comment line - '', // Empty line - ' ', // Whitespace only - ] - - invalidCases.forEach(testCase => { - const match = testCase.match(ENV_VAR_REGEX) - expect(match).toBeFalsy() - }) - }) - - it('should match environment variables with special characters in values', () => { - const specialValueCases = [ - 'KEY="quoted value"', - 'KEY=\'single quoted\'', - 'KEY=value#with#hashes', - 'KEY=value@with!special$chars', - 'KEY=http://example.com?query=param&other=value', - 'KEY=${OTHER_VAR}', - 'KEY=value;with;semicolons', - 'KEY=value,with,commas', - ] - - specialValueCases.forEach(testCase => { - const match = testCase.match(ENV_VAR_REGEX) - expect(match).toBeTruthy() - expect(match?.[0]).toBe(testCase) - }) - }) -}) diff --git a/src/regex.ts b/src/regex.ts deleted file mode 100644 index f20f423..0000000 --- a/src/regex.ts +++ /dev/null @@ -1 +0,0 @@ -export const ENV_VAR_REGEX = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/ diff --git a/tsconfig.json b/tsconfig.json index f0be713..6ceb7c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "declaration": true, "sourceMap": false, }, - "include": ["src"] + "include": ["src", "eslint.config.ts", "vitest.config.ts"] } From b4e6476e41869cd8b438a574d9c337b0ebd348ee Mon Sep 17 00:00:00 2001 From: Dak Washbrook Date: Mon, 13 Oct 2025 22:50:00 -0700 Subject: [PATCH 2/5] Refactor CI workflow to trigger only on pull requests --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10c0478..cd7016c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,7 @@ name: CI on: - push: - branches: ["**"] - pull_request: - branches: ["**"] + - pull_request jobs: test: From ce3cfb4dd455c1bda8df89a0420e0a8a731e8c58 Mon Sep 17 00:00:00 2001 From: Dak Washbrook Date: Mon, 13 Oct 2025 23:08:36 -0700 Subject: [PATCH 3/5] Add ESLint configuration files and update package scripts to use Bun for task execution --- eslint.config.d.ts | 2 + eslint.config.js | 101 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 22 +++++----- src/cli.test.ts | 54 +++++++++++------------ src/index.test.ts | 53 ++++++++++++++--------- src/index.ts | 19 ++++++--- tsconfig.build.json | 4 +- 7 files changed, 188 insertions(+), 67 deletions(-) create mode 100644 eslint.config.d.ts create mode 100644 eslint.config.js diff --git a/eslint.config.d.ts b/eslint.config.d.ts new file mode 100644 index 0000000..5db6659 --- /dev/null +++ b/eslint.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("@eslint/core", { with: { "resolution-mode": "require" } }).ConfigObject[]; +export default _default; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a8259d7 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,101 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import eslintPluginPrettier from 'eslint-plugin-prettier'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import { noAnyExceptInGenerics } from './eslint/no-any-except-in-generics'; +// ----------------------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------------------- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// ----------------------------------------------------------------------------------------- +// Export the flat config array using the `typescript-eslint` helper. This automatically +// wires up the correct parser/plugin and exposes the `strictTypeChecked` preset. +// ----------------------------------------------------------------------------------------- +export default defineConfig(globalIgnores(['node_modules/**', '**/dist', '**/*.js']), +// Base JavaScript rules. +js.configs.recommended, +// TypeScript rules - start with the recommended set and then enable the strict +// type-checked variant which performs full program-level analysis. +...tseslint.configs.recommended, ...tseslint.configs.strictTypeChecked, +// Disable strict type checking for custom ESLint rules +{ + files: ['eslint/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + }, +}, +// Provide project-aware parsing so that the strict presets have full type info. +{ + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: __dirname, + }, + }, +}, +// Prettier plugin + our opinionated overrides. +{ + plugins: { + prettier: eslintPluginPrettier, + }, + rules: { + 'prettier/prettier': 'error', + 'newline-before-return': 'error', + // ---------------------------------------------------------------------- + // Existing project-specific rule tweaks + // ---------------------------------------------------------------------- + camelcase: 'off', + 'import/prefer-default-export': 'off', + // Align with previous configuration - soften a few rules that are too strict + 'no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false, + }, + ], + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + // TypeScript-specific relaxations + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + }, +}, eslintConfigPrettier, +// Custom configs +defineConfig({ + files: ['src/**/*.ts'], + ignores: ['src/**/*.dts.ts'], + plugins: { + '@nuances': { + rules: { + 'no-any-except-in-generics': noAnyExceptInGenerics, + }, + }, + }, + rules: { + '@nuances/no-any-except-in-generics': 'error', + }, +})); diff --git a/package.json b/package.json index 629db08..d19d45b 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,17 @@ "url": "https://github.com/dak-engineering/synv/issues" }, "scripts": { - "build": "tsc -p tsconfig.build.json", - "dev": "tsx src/cli.ts", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "test": "vitest", - "test:ci": "vitest run", - "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage", - "changeset": "changeset", - "changeset:version": "changeset version && bun install --lockfile-only", - "changeset:publish": "changeset publish", + "build": "bun run tsc -p tsconfig.build.json", + "dev": "bun run tsx src/cli.ts", + "lint": "bun run eslint src", + "lint:fix": "bun run eslint src --fix", + "test": "bun run vitest", + "test:ci": "bun run vitest run", + "test:ui": "bun run vitest --ui", + "test:coverage": "bun run vitest run --coverage", + "changeset": "bun run changeset", + "changeset:version": "bun run changeset version && bun install --lockfile-only", + "changeset:publish": "bun run changeset publish", "release": "bun run build && bun run changeset:publish" }, "packageManager": "bun@1.3.0", diff --git a/src/cli.test.ts b/src/cli.test.ts index 39bacfa..59434b9 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,5 @@ import { execSync } from 'child_process' -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -10,7 +10,8 @@ describe('CLI End-to-End Tests', () => { beforeEach(() => { // Create a temporary directory for test files - tempDir = mkdtempSync(join(tmpdir(), 'synv-test-')) + // Use realpathSync to normalize the path (handles /var vs /private/var on macOS) + tempDir = realpathSync(mkdtempSync(join(tmpdir(), 'synv-test-'))) }) afterEach(() => { @@ -68,10 +69,9 @@ DB_URL="postgres://localhost"`, ) // Run CLI in non-interactive mode (using environment variable) - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) const result = readFileSync(envPath, 'utf-8') @@ -89,10 +89,9 @@ DB_URL="postgres://localhost"`, writeFileSync(customExamplePath, `API_KEY=""`) writeFileSync(customEnvPath, `API_KEY="secret123"`) - execSync(`node ${cliPath} -i .env.template -o .env.local`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}" -i .env.template -o .env.local`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) const result = readFileSync(customEnvPath, 'utf-8') @@ -105,10 +104,9 @@ DB_URL="postgres://localhost"`, writeFileSync(envExamplePath, `NEW_VAR="default"`) - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) expect(existsSync(envPath)).toBe(true) @@ -133,10 +131,9 @@ STRAY_B="b" STRAY_A="a"`, ) - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) const result = readFileSync(envPath, 'utf-8') @@ -156,10 +153,9 @@ STRAY_A="a"`, describe('Error handling', () => { it('should fail gracefully when .env.example does not exist', () => { try { - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) expect.fail('Should have thrown an error') } catch (error: unknown) { @@ -170,10 +166,9 @@ STRAY_A="a"`, it('should fail with custom example file path that does not exist', () => { try { - execSync(`node ${cliPath} -i .env.nonexistent`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}" -i .env.nonexistent`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) expect.fail('Should have thrown an error') } catch (error: unknown) { @@ -209,10 +204,9 @@ VAR3="custom3" VAR4="custom4"`, ) - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) const result = readFileSync(envPath, 'utf-8') @@ -240,10 +234,9 @@ EMPTY=""`, writeFileSync(envPath, `FILLED="has value"`) - execSync(`node ${cliPath}`, { - cwd: tempDir, + execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - env: { ...process.env, CI: 'true' }, + shell: true, }) const result = readFileSync(envPath, 'utf-8') @@ -257,7 +250,8 @@ describe('CLI Interactive Tests', () => { let tempDir: string beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'synv-interactive-test-')) + // Use realpathSync to normalize the path (handles /var vs /private/var on macOS) + tempDir = realpathSync(mkdtempSync(join(tmpdir(), 'synv-interactive-test-'))) }) afterEach(() => { @@ -270,14 +264,14 @@ describe('CLI Interactive Tests', () => { const envExamplePath = join(tempDir, '.env.example') const envPath = join(tempDir, '.env') - writeFileSync(envExamplePath, `CONFLICT_VAR="example_value"`) - writeFileSync(envPath, `CONFLICT_VAR="current_value"`) - // Note: In a real scenario, we would need to use a library like 'node-pty' // or 'execa' with stdin simulation to test interactive prompts. // For now, we're testing that the interactive mode would be triggered. // This test confirms the setup is correct for interactive mode + writeFileSync(envExamplePath, `CONFLICT_VAR="example_value"`) + writeFileSync(envPath, `CONFLICT_VAR="current_value"`) + expect(existsSync(envExamplePath)).toBe(true) expect(existsSync(envPath)).toBe(true) }) diff --git a/src/index.test.ts b/src/index.test.ts index d8cd0bd..d09e32e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -42,12 +42,12 @@ TURSO_DATABASE_URL="libsql://localhost:8080" TURSO_AUTH_TOKEN="1234"` mockedExistsSync.mockImplementation((path) => { - return path === '.env.example' || path === '.env' + return path.endsWith('.env.example') || path.endsWith('.env') }) mockedReadFileSync.mockImplementation((path) => { - if (path === '.env.example') return envExampleContent - if (path === '.env') return envContent + if (path.endsWith('.env.example')) return envExampleContent + if (path.endsWith('.env')) return envContent throw new Error('File not found') }) @@ -63,7 +63,10 @@ DRAFT_MODE_SECRET="abc123" TURSO_DATABASE_URL="libsql://localhost:8080" TURSO_AUTH_TOKEN="1234"` - expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + expect(mockedWriteFileSync).toHaveBeenCalledWith( + expect.stringContaining('.env'), + expectedOutput, + ) }) it('should handle stray environment variables', async () => { @@ -76,12 +79,12 @@ STRAY_VAR_A="value_a" ANOTHER_VAR="another_value"` mockedExistsSync.mockImplementation((path) => { - return path === '.env.example' || path === '.env' + return path.endsWith('.env.example') || path.endsWith('.env') }) mockedReadFileSync.mockImplementation((path) => { - if (path === '.env.example') return envExampleContent - if (path === '.env') return envContent + if (path.endsWith('.env.example')) return envExampleContent + if (path.endsWith('.env')) return envContent throw new Error('File not found') }) @@ -95,7 +98,10 @@ ANOTHER_VAR="another_value" STRAY_VAR_A="value_a" STRAY_VAR_B="value_b"` - expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + expect(mockedWriteFileSync).toHaveBeenCalledWith( + expect.stringContaining('.env'), + expectedOutput, + ) }) it('should create .env file if it does not exist', async () => { @@ -106,11 +112,11 @@ NEXT_PUBLIC_APP_HOST="http://localhost:3000" PAYLOAD_SECRET="abc123"` mockedExistsSync.mockImplementation((path) => { - return path === '.env.example' + return path.endsWith('.env.example') }) mockedReadFileSync.mockImplementation((path) => { - if (path === '.env.example') return envExampleContent + if (path.endsWith('.env.example')) return envExampleContent throw new Error('File not found') }) @@ -121,7 +127,10 @@ NEXT_PUBLIC_APP_HOST="http://localhost:3000" # CMS PAYLOAD_SECRET="abc123"` - expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + expect(mockedWriteFileSync).toHaveBeenCalledWith( + expect.stringContaining('.env'), + expectedOutput, + ) }) it('should handle custom file paths', async () => { @@ -129,12 +138,12 @@ PAYLOAD_SECRET="abc123"` const envContent = `KEY="different"` mockedExistsSync.mockImplementation((path) => { - return path === '.env.custom.example' || path === '.env.custom' + return path.endsWith('.env.custom.example') || path.endsWith('.env.custom') }) mockedReadFileSync.mockImplementation((path) => { - if (path === '.env.custom.example') return envExampleContent - if (path === '.env.custom') return envContent + if (path.endsWith('.env.custom.example')) return envExampleContent + if (path.endsWith('.env.custom')) return envContent throw new Error('File not found') }) @@ -144,7 +153,10 @@ PAYLOAD_SECRET="abc123"` interactive: false, }) - expect(mockedWriteFileSync).toHaveBeenCalledWith('.env.custom', 'KEY="different"') + expect(mockedWriteFileSync).toHaveBeenCalledWith( + expect.stringContaining('.env.custom'), + 'KEY="different"', + ) }) it('should throw error if .env.example does not exist', async () => { @@ -163,12 +175,12 @@ API_KEY=""` API_KEY=""` mockedExistsSync.mockImplementation((path) => { - return path === '.env.example' || path === '.env' + return path.endsWith('.env.example') || path.endsWith('.env') }) mockedReadFileSync.mockImplementation((path) => { - if (path === '.env.example') return envExampleContent - if (path === '.env') return envContent + if (path.endsWith('.env.example')) return envExampleContent + if (path.endsWith('.env')) return envContent throw new Error('File not found') }) @@ -177,6 +189,9 @@ API_KEY=""` const expectedOutput = `DATABASE_URL="postgres://localhost" API_KEY=""` - expect(mockedWriteFileSync).toHaveBeenCalledWith('.env', expectedOutput) + expect(mockedWriteFileSync).toHaveBeenCalledWith( + expect.stringContaining('.env'), + expectedOutput, + ) }) }) diff --git a/src/index.ts b/src/index.ts index f17fb5a..c05079a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from 'fs' +import { resolve } from 'path' import { input, select } from '@inquirer/prompts' import chalk from 'chalk' import ora from 'ora' @@ -108,16 +109,17 @@ async function resolveValue( } export async function syncEnvFiles(options: SyncOptions = {}): Promise { - const envExampleFile = options.envExampleFile || '.env.example' - const envFile = options.envFile || '.env' + const envExampleFile = resolve(process.cwd(), options.envExampleFile || '.env.example') + const envFile = resolve(process.cwd(), options.envFile || '.env') const interactive = options.interactive ?? true const spinner = interactive ? ora('Reading environment files...').start() : null try { if (!existsSync(envExampleFile)) { - spinner?.fail(`${envExampleFile} file not found`) - throw new Error(`${envExampleFile} file not found`) + const displayPath = options.envExampleFile || '.env.example' + spinner?.fail(`${displayPath} file not found`) + throw new Error(`${displayPath} file not found`) } const exampleContent = readFileSync(envExampleFile, 'utf-8') @@ -182,14 +184,19 @@ export async function syncEnvFiles(options: SyncOptions = {}): Promise { const finalContent = output.join('\n') writeFileSync(envFile, finalContent) + const displayEnvFile = options.envFile || '.env' + const displayEnvExampleFile = options.envExampleFile || '.env.example' + writeSpinner?.succeed( - chalk.green(`Successfully synced ${chalk.bold(envFile)} with ${chalk.bold(envExampleFile)}`), + chalk.green( + `Successfully synced ${chalk.bold(displayEnvFile)} with ${chalk.bold(displayEnvExampleFile)}`, + ), ) if (strayEntries.length > 0 && interactive) { console.log( chalk.yellow( - `\n⚠ ${String(strayEntries.length)} additional variables were moved to the bottom of ${envFile}`, + `\n⚠ ${String(strayEntries.length)} additional variables were moved to the bottom of ${displayEnvFile}`, ), ) } diff --git a/tsconfig.build.json b/tsconfig.build.json index c31aea5..6d7f751 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,12 +6,14 @@ "module": "NodeNext", "moduleResolution": "NodeNext" }, + "include": ["src/**/*"], "exclude": [ "**/*.test.ts", "**/*.spec.ts", "node_modules", "dist", "coverage", - "vitest.config.ts" + "vitest.config.ts", + "eslint.config.ts" ] } From 6debfae4499e5dd77988599159e595d10723da7e Mon Sep 17 00:00:00 2001 From: Dak Washbrook Date: Mon, 13 Oct 2025 23:12:44 -0700 Subject: [PATCH 4/5] Update CLI tests to use '/bin/sh' as the shell for executing commands --- src/cli.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 59434b9..0f0023a 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -71,7 +71,7 @@ DB_URL="postgres://localhost"`, // Run CLI in non-interactive mode (using environment variable) execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) const result = readFileSync(envPath, 'utf-8') @@ -91,7 +91,7 @@ DB_URL="postgres://localhost"`, execSync(`cd "${tempDir}" && CI=true node "${cliPath}" -i .env.template -o .env.local`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) const result = readFileSync(customEnvPath, 'utf-8') @@ -106,7 +106,7 @@ DB_URL="postgres://localhost"`, execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) expect(existsSync(envPath)).toBe(true) @@ -133,7 +133,7 @@ STRAY_A="a"`, execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) const result = readFileSync(envPath, 'utf-8') @@ -155,7 +155,7 @@ STRAY_A="a"`, try { execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) expect.fail('Should have thrown an error') } catch (error: unknown) { @@ -168,7 +168,7 @@ STRAY_A="a"`, try { execSync(`cd "${tempDir}" && CI=true node "${cliPath}" -i .env.nonexistent`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) expect.fail('Should have thrown an error') } catch (error: unknown) { @@ -206,7 +206,7 @@ VAR4="custom4"`, execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) const result = readFileSync(envPath, 'utf-8') @@ -236,7 +236,7 @@ EMPTY=""`, execSync(`cd "${tempDir}" && CI=true node "${cliPath}"`, { encoding: 'utf-8', - shell: true, + shell: '/bin/sh', }) const result = readFileSync(envPath, 'utf-8') From a3adb21200ccb2d4c6f464d2b55671b653e48df3 Mon Sep 17 00:00:00 2001 From: Dak Washbrook Date: Mon, 13 Oct 2025 23:24:15 -0700 Subject: [PATCH 5/5] Reorganize CI workflow to move coverage tests after the build step --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd7016c..21b6bf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,6 @@ jobs: - name: Type check run: bun tsc --noEmit - - name: Run tests with coverage - run: bun test:coverage - - name: Build package run: bun run build @@ -35,3 +32,6 @@ jobs: exit 1 fi echo "βœ“ Build output verified" + + - name: Run tests with coverage + run: bun test:coverage