├── .editor └── .gitignore ├── .editorconfig ├── .github ├── CODEOWNERS ├── release.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── web.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bun.lock ├── dist └── .gitignore ├── eslint.config.mjs ├── logo.png ├── logo.svg ├── package.json ├── rollup.config.js ├── src ├── cli.ts ├── index.ts └── lib │ ├── configparser │ └── index.ts │ ├── filewalker │ └── index.ts │ └── parser │ ├── BrunoDocumentParser.ts │ ├── BrunoToJson.ts │ ├── Diff.ts │ ├── DocumentBuilder.ts │ ├── DocumentParser.ts │ ├── OpenAPIDocumentParser.ts │ ├── PostmanDocumentParser.ts │ └── index.ts ├── tsconfig.json └── web ├── .envrc ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .yamllint.yaml ├── README.md ├── bun.lock ├── eslint.config.js ├── package.json ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── HeadComponent.svelte │ └── index.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ └── +page.svelte ├── static ├── favicon.png ├── logo.png ├── logo.svg └── schema.json ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.editor/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | 10 | [*.go] 11 | indent_style = tab 12 | insert_final_newline = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gorillamoe 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 💥 7 | labels: 8 | - Semver-Major 9 | - breaking-change 10 | - title: Exciting New Features ✨ 11 | labels: 12 | - Semver-Minor 13 | - enhancement 14 | - title: Bug Fixes 🐛 15 | labels: 16 | - Semver-Patch 17 | - bug 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: read 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Set up env 22 | run: | 23 | VERSION=${GITHUB_REF_NAME#v} 24 | echo "VERSION=$VERSION" >> $GITHUB_ENV 25 | - name: Set up Bun 26 | uses: oven-sh/setup-bun@v2 27 | - name: Cache Bun 28 | uses: actions/cache@v4 29 | with: 30 | path: ~/.bun/install/cache 31 | key: linux-bun-${{ hashFiles('**/bun.lock') }} 32 | restore-keys: | 33 | linux-bun-${{ hashFiles('**/bun.lock') }} 34 | - name: Install dependencies 35 | run: bun install --frozen-lockfile 36 | - name: Lint 37 | run: bun run lint 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+' 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Set up env 17 | run: | 18 | VERSION=${GITHUB_REF_NAME#v} 19 | echo "VERSION=$VERSION" >> $GITHUB_ENV 20 | - name: Set up Bun 21 | uses: oven-sh/setup-bun@v2 22 | - name: Cache Bun 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.bun/install/cache 26 | key: linux-bun-${{ hashFiles('**/bun.lock') }} 27 | restore-keys: | 28 | linux-bun-${{ hashFiles('**/bun.lock') }} 29 | - name: Install dependencies 30 | run: bun install --frozen-lockfile 31 | - name: Build 32 | run: bun run build 33 | - name: Publish 34 | run: bun publish 35 | env: 36 | NPM_CONFIG_TOKEN: ${{ secrets.NPM_CONFIG_TOKEN }} 37 | - name: Make release 38 | run: | 39 | gh release create v$VERSION -t "v$VERSION" --generate-notes 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/web.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy website 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'web/**' 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Allow only one concurrent deployment, 15 | # skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want 17 | # to allow these production deployments to complete. 18 | concurrency: 19 | group: "web" 20 | cancel-in-progress: false 21 | 22 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 23 | permissions: 24 | contents: write 25 | pages: write 26 | id-token: write 27 | 28 | jobs: 29 | build-linux: 30 | name: Deploy website 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | - name: Set up Bun 38 | uses: oven-sh/setup-bun@v2 39 | - name: Cache Bun 40 | uses: actions/cache@v4 41 | with: 42 | path: ~/.bun/install/cache 43 | key: linux-bun-web-${{ hashFiles('**/bun.lock') }} 44 | restore-keys: | 45 | linux-bun-web-${{ hashFiles('**/bun.lock') }} 46 | - name: Install web dependencies 47 | run: cd web && bun install --frozen-lockfile 48 | - name: Create Website 49 | run: cd web && bun run build 50 | - name: Setup Pages 51 | uses: actions/configure-pages@v5 52 | - name: Upload artifact 53 | uses: actions/upload-pages-artifact@v3 54 | with: 55 | path: "web/build" 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Go workspace file 15 | go.work 16 | 17 | .env 18 | http-client.env.json 19 | http-client.private.env.json 20 | *.rest 21 | *.http 22 | 23 | node_modules 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025+ mistweaverco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Kulala-fmt Logo](logo.svg) 4 | 5 | # kulala-fmt 6 | 7 | [![NPM](https://img.shields.io/npm/v/@mistweaverco/kulala-fmt?style=for-the-badge)](https://www.npmjs.com/package/@mistweaverco/kulala-fmt) 8 | [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6.svg?style=for-the-badge&logo=typescript&logoColor=FFF)](https://www.typescriptlang.org/) 9 | [![Rollup](https://img.shields.io/badge/Rollup-bd0f0f.svg?style=for-the-badge&logo=rollup.js&logoColor=FFF)](https://rollupjs.org/) 10 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/mistweaverco/kulala-fmt?style=for-the-badge)](https://github.com/mistweaverco/kulala-fmt/releases/latest) 11 | [![Discord](https://img.shields.io/badge/discord-join-7289da?style=for-the-badge&logo=discord)](https://discord.gg/QyVQmfY4Rt) 12 | 13 | [Install](#install) • [Usage](#usage) 14 | 15 |

16 | 17 | An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡. 18 | 19 |

20 | 21 |
22 | 23 | ## Install 24 | 25 | Via npm: 26 | 27 | ```sh 28 | npm install -g @mistweaverco/kulala-fmt 29 | ``` 30 | 31 | ## Usage 32 | 33 | kulala-fmt can `format` and `check` `.http` and `.rest` files. 34 | 35 | It can also `convert` OpenAPI `.yaml`, `.yml` or `.json` files to `.http` files. 36 | 37 | ### Format 38 | 39 | Format all `.http` and `.rest` files in the current directory and its subdirectories: 40 | 41 | ```sh 42 | kulala-fmt format 43 | ``` 44 | 45 | Format specific `.http` and `.rest` files. 46 | 47 | ```sh 48 | kulala-fmt format file1.http file2.rest http/*.http 49 | ``` 50 | 51 | Format stdin input: 52 | 53 | ```sh 54 | cat SOMEFILE.http | kulala-fmt format --stdin 55 | ``` 56 | 57 | ### Check 58 | 59 | Check if all `.http` and `.rest` files in the current directory and its subdirectories are formatted: 60 | 61 | ```sh 62 | kulala-fmt check 63 | ``` 64 | 65 | Check if specific `.http` and `.rest` files are formatted: 66 | 67 | ```sh 68 | kulala-fmt check file1.http file2.rest http/*.http 69 | ``` 70 | 71 | Check if all `.http` and `.rest` files in the current directory and 72 | its subdirectories are formatted and 73 | prints the desired output to the console: 74 | 75 | ```sh 76 | kulala-fmt check --verbose 77 | ``` 78 | 79 | Check if specific `.http` and `.rest` files are formatted and 80 | prints the desired output to the console: 81 | 82 | ```sh 83 | kulala-fmt check --verbose file1.http file2.rest http/*.http 84 | ``` 85 | 86 | Check stdin input: 87 | 88 | ```sh 89 | cat SOMEFILE.http | kulala-fmt format --stdin 90 | ``` 91 | 92 | ### Convert 93 | 94 | #### OpenAPI to `.http` 95 | 96 | Convert OpenAPI `.yaml`, `.yml` or `.json` files to `.http` files: 97 | 98 | ```sh 99 | kulala-fmt convert --from openapi openapi.yaml 100 | ``` 101 | 102 | #### Postman collection to `.http` 103 | 104 | Convert Postman collection `.json` files to `.http` files: 105 | 106 | ```sh 107 | kulala-fmt convert --from postman postman.json 108 | ``` 109 | 110 | #### Bruno to `.http` 111 | 112 | Convert Bruno collections to `.http` files: 113 | 114 | ```sh 115 | kulala-fmt convert --from bruno path/to/bruno/collection 116 | ``` 117 | 118 | ## What does it do? 119 | 120 | - Checks if the file is formatted and valid 121 | - Removes extraneous newlines 122 | - Makes sure document variables are at the top of the file 123 | - Lowercases all headers (when HTTP/2 or HTTP/3) else it will uppercase the first letter 124 | - Puts all metadata right before the request line 125 | - Ensures all comments are at the top of the request 126 | 127 | So a perfect request would look like this: 128 | 129 | ```http 130 | @variables1 = value1 131 | 132 | 133 | ### REQUEST_NAME_ONE 134 | 135 | # This is a comment 136 | # This is another comment 137 | # @someother metatag 138 | GET http://localhost:8080/api/v1/health HTTP/1.1 139 | Content-Type: application/json 140 | 141 | { 142 | "key": "value" 143 | } 144 | ``` 145 | 146 | or this: 147 | 148 | ```http 149 | @variables1 = value1 150 | 151 | 152 | ### REQUEST_NAME_ONE 153 | 154 | # This is a comment 155 | # This is another comment 156 | # @someother metatag 157 | GET http://localhost:8080/api/v1/health HTTP/2 158 | content-type: application/json 159 | 160 | { 161 | "key": "value" 162 | } 163 | ``` 164 | 165 | ## Use it with conform.nvim 166 | 167 | ```lua 168 | return { 169 | "stevearc/conform.nvim", 170 | config = function() 171 | require("conform").setup({ 172 | formatters = { 173 | kulala = { 174 | command = "kulala-fmt", 175 | args = { "format", "$FILENAME" }, 176 | stdin = false, 177 | }, 178 | }, 179 | formatters_by_ft = { 180 | http = { "kulala" }, 181 | }, 182 | format_on_save = true, 183 | }) 184 | end, 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "@mistweaverco/kulala-fmt", 6 | "dependencies": { 7 | "@mistweaverco/tree-sitter-kulala": "1.9.0", 8 | "prettier": "^3.5.2", 9 | "tree-sitter": "0.22.4", 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^28.0.2", 13 | "@rollup/plugin-json": "^6.1.0", 14 | "@rollup/plugin-node-resolve": "^16.0.0", 15 | "@rollup/plugin-typescript": "^12.1.2", 16 | "@types/bun": "latest", 17 | "@types/diff": "^7.0.1", 18 | "@types/js-yaml": "^4.0.9", 19 | "@typescript-eslint/eslint-plugin": "^8.24.1", 20 | "@typescript-eslint/parser": "^8.24.1", 21 | "colors": "^1.4.0", 22 | "commander": "^13.1.0", 23 | "diff": "^7.0.0", 24 | "eslint": "^9.20.1", 25 | "eslint-config-prettier": "^10.0.1", 26 | "eslint-plugin-prettier": "^5.2.3", 27 | "js-yaml": "^4.1.0", 28 | "lodash": "^4.17.21", 29 | "ohm-js": "^17.1.0", 30 | "rollup": "^4.34.8", 31 | "tslib": "^2.8.1", 32 | "typescript-eslint": "^8.24.1", 33 | }, 34 | "peerDependencies": { 35 | "typescript": "^5.7.3", 36 | }, 37 | }, 38 | }, 39 | "trustedDependencies": [ 40 | "@mistweaverco/tree-sitter-kulala", 41 | ], 42 | "packages": { 43 | "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], 44 | 45 | "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 46 | 47 | "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], 48 | 49 | "@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="], 50 | 51 | "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "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-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], 52 | 53 | "@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="], 54 | 55 | "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 56 | 57 | "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], 58 | 59 | "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 60 | 61 | "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], 62 | 63 | "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 64 | 65 | "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], 66 | 67 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 68 | 69 | "@mistweaverco/tree-sitter-kulala": ["@mistweaverco/tree-sitter-kulala@1.9.0", "", { "dependencies": { "node-addon-api": "8.1.0", "node-gyp-build": "4.8.2" } }, "sha512-4F+kCZFEvurSGrWM52lnowXBOqQ5+nfzxp55ufCqTdDSzSI92kySHmlhF7d2g/ZW4Jjrbromh1WH9XK/mHPbzQ=="], 70 | 71 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 72 | 73 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 74 | 75 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 76 | 77 | "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], 78 | 79 | "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw=="], 80 | 81 | "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], 82 | 83 | "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg=="], 84 | 85 | "@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.1.2", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg=="], 86 | 87 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], 88 | 89 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="], 90 | 91 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="], 92 | 93 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="], 94 | 95 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="], 96 | 97 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="], 98 | 99 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="], 100 | 101 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="], 102 | 103 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="], 104 | 105 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="], 106 | 107 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="], 108 | 109 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="], 110 | 111 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="], 112 | 113 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="], 114 | 115 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="], 116 | 117 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="], 118 | 119 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="], 120 | 121 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="], 122 | 123 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="], 124 | 125 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="], 126 | 127 | "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], 128 | 129 | "@types/diff": ["@types/diff@7.0.1", "", {}, "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA=="], 130 | 131 | "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], 132 | 133 | "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], 134 | 135 | "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 136 | 137 | "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="], 138 | 139 | "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], 140 | 141 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], 142 | 143 | "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="], 144 | 145 | "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="], 146 | 147 | "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="], 148 | 149 | "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="], 150 | 151 | "@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="], 152 | 153 | "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.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.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="], 154 | 155 | "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="], 156 | 157 | "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="], 158 | 159 | "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], 160 | 161 | "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 162 | 163 | "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=="], 164 | 165 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 166 | 167 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 168 | 169 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 170 | 171 | "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], 172 | 173 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 174 | 175 | "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], 176 | 177 | "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 178 | 179 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 180 | 181 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 182 | 183 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 184 | 185 | "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], 186 | 187 | "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], 188 | 189 | "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], 190 | 191 | "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 192 | 193 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 194 | 195 | "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 196 | 197 | "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 198 | 199 | "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], 200 | 201 | "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], 202 | 203 | "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 204 | 205 | "eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@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.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.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-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="], 206 | 207 | "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="], 208 | 209 | "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="], 210 | 211 | "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], 212 | 213 | "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], 214 | 215 | "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], 216 | 217 | "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], 218 | 219 | "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 220 | 221 | "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 222 | 223 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 224 | 225 | "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 226 | 227 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 228 | 229 | "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], 230 | 231 | "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=="], 232 | 233 | "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 234 | 235 | "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 236 | 237 | "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], 238 | 239 | "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], 240 | 241 | "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 242 | 243 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 244 | 245 | "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 246 | 247 | "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 248 | 249 | "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 250 | 251 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 252 | 253 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 254 | 255 | "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 256 | 257 | "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 258 | 259 | "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 260 | 261 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 262 | 263 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 264 | 265 | "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 266 | 267 | "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 268 | 269 | "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 270 | 271 | "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 272 | 273 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 274 | 275 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 276 | 277 | "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], 278 | 279 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 280 | 281 | "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], 282 | 283 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 284 | 285 | "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], 286 | 287 | "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 288 | 289 | "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 290 | 291 | "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 292 | 293 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 294 | 295 | "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], 296 | 297 | "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 298 | 299 | "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 300 | 301 | "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 302 | 303 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], 304 | 305 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 306 | 307 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 308 | 309 | "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 310 | 311 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 312 | 313 | "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 314 | 315 | "node-addon-api": ["node-addon-api@8.1.0", "", {}, "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ=="], 316 | 317 | "node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-test": "build-test.js", "node-gyp-build-optional": "optional.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="], 318 | 319 | "ohm-js": ["ohm-js@17.1.0", "", {}, "sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q=="], 320 | 321 | "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=="], 322 | 323 | "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 324 | 325 | "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 326 | 327 | "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 328 | 329 | "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 330 | 331 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 332 | 333 | "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], 334 | 335 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 336 | 337 | "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 338 | 339 | "prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="], 340 | 341 | "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], 342 | 343 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 344 | 345 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 346 | 347 | "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], 348 | 349 | "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 350 | 351 | "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], 352 | 353 | "rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="], 354 | 355 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 356 | 357 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 358 | 359 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 360 | 361 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 362 | 363 | "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 364 | 365 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 366 | 367 | "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 368 | 369 | "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="], 370 | 371 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 372 | 373 | "tree-sitter": ["tree-sitter@0.22.4", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg=="], 374 | 375 | "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], 376 | 377 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 378 | 379 | "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 380 | 381 | "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], 382 | 383 | "typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="], 384 | 385 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 386 | 387 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 388 | 389 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 390 | 391 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 392 | 393 | "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 394 | 395 | "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 396 | 397 | "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], 398 | 399 | "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], 400 | 401 | "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 402 | 403 | "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 404 | 405 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 406 | 407 | "tree-sitter/node-addon-api": ["node-addon-api@8.3.1", "", {}, "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="], 408 | 409 | "tree-sitter/node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], 410 | 411 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import * as eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | 6 | export default [ 7 | { files: ["**/*.{js,mjs,cjs,ts}"] }, 8 | { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, 9 | { languageOptions: { globals: globals.browser } }, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | eslintPluginPrettierRecommended.default, 13 | { 14 | ignores: ["dist/**/*", "web/**/*", "node_modules/**/*"], 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mistweaverco/kulala-fmt", 3 | "version": "2.10.0", 4 | "type": "module", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "lint": "eslint ." 8 | }, 9 | "bin": { 10 | "kulala-fmt": "dist/cli.cjs" 11 | }, 12 | "files": [ 13 | "dist/cli.cjs" 14 | ], 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "dependencies": { 19 | "@mistweaverco/tree-sitter-kulala": "1.9.0", 20 | "tree-sitter": "0.22.4", 21 | "prettier": "^3.5.2" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-commonjs": "^28.0.2", 25 | "@rollup/plugin-json": "^6.1.0", 26 | "@rollup/plugin-node-resolve": "^16.0.0", 27 | "@rollup/plugin-typescript": "^12.1.2", 28 | "@types/bun": "latest", 29 | "@types/diff": "^7.0.1", 30 | "@types/js-yaml": "^4.0.9", 31 | "@typescript-eslint/eslint-plugin": "^8.24.1", 32 | "@typescript-eslint/parser": "^8.24.1", 33 | "colors": "^1.4.0", 34 | "commander": "^13.1.0", 35 | "diff": "^7.0.0", 36 | "eslint": "^9.20.1", 37 | "eslint-config-prettier": "^10.0.1", 38 | "eslint-plugin-prettier": "^5.2.3", 39 | "js-yaml": "^4.1.0", 40 | "lodash": "^4.17.21", 41 | "ohm-js": "^17.1.0", 42 | "rollup": "^4.34.8", 43 | "tslib": "^2.8.1", 44 | "typescript-eslint": "^8.24.1" 45 | }, 46 | "peerDependencies": { 47 | "typescript": "^5.7.3" 48 | }, 49 | "trustedDependencies": [ 50 | "@mistweaverco/tree-sitter-kulala" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from "@rollup/plugin-json"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | const config = [ 6 | { 7 | input: "src/index.ts", 8 | output: { 9 | file: "dist/cli.cjs", 10 | format: "cjs", 11 | sourcemap: false, 12 | }, 13 | external: ["tree-sitter", "@mistweaverco/tree-sitter-kulala", "prettier"], 14 | plugins: [ 15 | json(), 16 | commonjs(), 17 | typescript({ 18 | tsconfig: "./tsconfig.json", 19 | noForceEmit: true, 20 | }), 21 | resolve({ 22 | preferBuiltins: true, 23 | }), 24 | ], 25 | }, 26 | ]; 27 | export default config; 28 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import pkg from "./../package.json"; 2 | import { Command } from "commander"; 3 | import { check, format, convert } from "./lib/parser"; 4 | import { configparser } from "./lib/configparser"; 5 | const program = new Command(); 6 | 7 | program 8 | .name("kulala-fmt") 9 | .description( 10 | "An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡.", 11 | ) 12 | .version(pkg.version); 13 | 14 | program 15 | .command("format") 16 | .description("Format files") 17 | .argument("[files]", "files to include", null) 18 | .option("--body", "also format the body", true) 19 | .option("--stdin", "read input from stdin, print output to stdout", false) 20 | .action(async (files, options) => { 21 | await format(files, options); 22 | }); 23 | 24 | program 25 | .command("check") 26 | .description("Check if files are well formatted") 27 | .argument("[files]", "files to include", null) 28 | .option("-v, --verbose", "enable verbose mode", false) 29 | .option("--body", "also format the body", true) 30 | .option("--stdin", "read input from stdin", false) 31 | .action(async (files, options) => { 32 | await check(files, options); 33 | }); 34 | 35 | program 36 | .command("convert") 37 | .description("Convert files to .http format") 38 | .argument("", "files to include") 39 | .option("--from ", "source format", "openapi") 40 | .option("--to ", "destination format", "http") 41 | .action(async (files, options) => { 42 | await convert(options, files); 43 | }); 44 | 45 | program 46 | .command("init") 47 | .description("initialize a new kulala-fmt.yaml file") 48 | .action(() => { 49 | configparser.init(); 50 | }); 51 | 52 | program.parse(); 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "./cli"; 4 | -------------------------------------------------------------------------------- /src/lib/configparser/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import path from "path"; 3 | import yaml from "js-yaml"; 4 | import chalk from "chalk"; 5 | import readline from "readline"; 6 | 7 | const CONFIG_FILENAME = "kulala-fmt.yaml"; 8 | 9 | export interface Config { 10 | defaults: { 11 | http_method: string; 12 | http_version: string; 13 | }; 14 | } 15 | 16 | const DEFAULT_CONFIG: Config = { 17 | defaults: { 18 | http_method: "GET", 19 | http_version: "HTTP/1.1", 20 | }, 21 | }; 22 | 23 | const init = (): void => { 24 | const file = path.join(process.cwd(), CONFIG_FILENAME); 25 | const configHeader = `# yaml-language-server: $schema=https://fmt.getkulala.net/schema.json\n---\n`; 26 | if (fs.existsSync(file)) { 27 | console.log(chalk.red(`🦄 Config file already exists: ${file}`)); 28 | const rl = readline.createInterface({ 29 | input: process.stdin, 30 | output: process.stdout, 31 | }); 32 | rl.question( 33 | `Do you want to overwrite the file? (y/N) `, 34 | (answer: string) => { 35 | if (answer.toLowerCase() === "y") { 36 | fs.writeFileSync(file, configHeader + yaml.dump(DEFAULT_CONFIG)); 37 | console.log(chalk.green(`🦄 Config file written: ${file}`)); 38 | } else if (answer.toLowerCase() === "n") { 39 | console.log(chalk.yellow("🦄 Exiting...")); 40 | } else { 41 | console.log(chalk.red("🦄 Invalid input")); 42 | } 43 | rl.close(); 44 | }, 45 | ); 46 | } else { 47 | fs.writeFileSync(file, configHeader + yaml.dump(DEFAULT_CONFIG)); 48 | console.log(chalk.green(`🦄 Config file written: ${file}`)); 49 | } 50 | }; 51 | 52 | const parse = (): Config => { 53 | const file = path.join(process.cwd(), CONFIG_FILENAME); 54 | if (!fs.existsSync(file)) { 55 | return DEFAULT_CONFIG; 56 | } 57 | const content = fs.readFileSync(file, "utf8"); 58 | const json = yaml.load(content) as Config; 59 | return { ...DEFAULT_CONFIG, ...json }; 60 | }; 61 | 62 | export const configparser = { 63 | init, 64 | parse, 65 | }; 66 | -------------------------------------------------------------------------------- /src/lib/filewalker/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import chalk from "chalk"; 4 | 5 | /** 6 | * Walks a directory recursively (or processes a single file) and returns an array of file paths 7 | * matching the given extensions. 8 | * 9 | * @param {string} inputPath The starting directory path or a file path. 10 | * @param {string[]} extensions An array of file extensions to filter by (e.g., ['.http', '.rest']). 11 | * @returns {string[]} An array of file paths matching the extensions. 12 | */ 13 | export function fileWalker(inputPath: string, extensions: string[]): string[] { 14 | const filePaths: string[] = []; 15 | const absolutePath = path.resolve(inputPath); 16 | let stats: fs.Stats; 17 | try { 18 | stats = fs.lstatSync(absolutePath); 19 | } catch (err: unknown) { 20 | const error = err as Error; 21 | console.error(chalk.red(`Error: ${error.message}`)); 22 | return filePaths; 23 | } 24 | 25 | // Define a root directory for computing relative paths. 26 | const rootDir: string = process.cwd(); 27 | if (stats.isFile()) { 28 | const ext = path.extname(absolutePath).toLowerCase(); 29 | if (extensions.includes(ext)) { 30 | // Return the file path relative to its parent directory. 31 | filePaths.push(path.relative(rootDir, absolutePath)); 32 | } 33 | return filePaths; 34 | } else if (stats.isDirectory()) { 35 | // nothing to do here 36 | } else { 37 | // If it's neither a file nor a directory (e.g., a special file), return empty. 38 | return filePaths; 39 | } 40 | 41 | // Recursive directory walker. 42 | function walk(currentPath: string) { 43 | const files = fs.readdirSync(currentPath); 44 | for (const file of files) { 45 | const filePath = path.join(currentPath, file); 46 | const stats = fs.lstatSync(filePath); 47 | 48 | // Skip symbolic links 49 | if (stats.isSymbolicLink()) { 50 | continue; 51 | } 52 | 53 | if (stats.isDirectory()) { 54 | walk(filePath); // Recursive call for subdirectories 55 | } else if (stats.isFile()) { 56 | const ext = path.extname(filePath).toLowerCase(); 57 | if (extensions.includes(ext)) { 58 | // Compute the relative path with respect to the root directory. 59 | filePaths.push(path.relative(rootDir, filePath)); 60 | } 61 | } 62 | } 63 | } 64 | 65 | walk(rootDir); 66 | return filePaths; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/parser/BrunoDocumentParser.ts: -------------------------------------------------------------------------------- 1 | import { BrunoToJSONParser } from "./BrunoToJson"; 2 | import { readFileSync, readdirSync, statSync } from "fs"; 3 | import { join, relative } from "path"; 4 | import type { Document, Block, Header, Variable } from "./DocumentParser"; 5 | 6 | interface BrunoCollection { 7 | version: string; 8 | name: string; 9 | type: "collection"; 10 | ignore?: string[]; 11 | } 12 | 13 | interface BrunoEnvironmentVars { 14 | vars?: Record; 15 | } 16 | 17 | interface EnvironmentInfo { 18 | name: string; 19 | vars: Record; 20 | } 21 | 22 | interface BrunoRequest { 23 | meta?: { 24 | name?: string; 25 | url?: string; 26 | }; 27 | http?: { 28 | method?: string; 29 | url?: string; 30 | }; 31 | headers?: BrunoHeader[]; 32 | body?: { 33 | json?: string; 34 | graphql?: { 35 | query?: string; 36 | variables?: string; 37 | }; 38 | formUrlEncoded?: Record; 39 | multipartForm?: Array<{ 40 | name: string; 41 | value: string; 42 | type?: string; 43 | }>; 44 | }; 45 | "script:pre-request"?: string; 46 | tests?: string; 47 | } 48 | 49 | interface ParseResult { 50 | documents: Document[]; 51 | environmentNames: string[]; 52 | collectionName: string; 53 | } 54 | 55 | interface BrunoHeader { 56 | name: string; 57 | value: string; 58 | enabled?: boolean; 59 | } 60 | 61 | let collectionInfo: BrunoCollection | undefined; 62 | 63 | export class BrunoDocumentParser { 64 | private parseEnvironmentBruFile(content: string): BrunoEnvironmentVars { 65 | const lines = content.split("\n"); 66 | const environment: BrunoEnvironmentVars = { vars: {} }; 67 | let isInVarsBlock = false; 68 | 69 | for (const line of lines) { 70 | const trimmedLine = line.trim(); 71 | 72 | if (!trimmedLine) continue; 73 | 74 | if (trimmedLine === "vars {") { 75 | isInVarsBlock = true; 76 | continue; 77 | } else if (trimmedLine === "}" && isInVarsBlock) { 78 | isInVarsBlock = false; 79 | continue; 80 | } 81 | 82 | if (isInVarsBlock && environment.vars) { 83 | const [key, ...valueParts] = trimmedLine 84 | .split(":") 85 | .map((part) => part.trim()); 86 | const value = valueParts.join(":").replace(/^"(.*)"$/, "$1"); // Remove quotes if present 87 | if (key && value) { 88 | environment.vars[key] = value; 89 | } 90 | } 91 | } 92 | 93 | return environment; 94 | } 95 | 96 | private buildRequestBlock( 97 | bruRequest: BrunoRequest, 98 | folderPath: string, 99 | ): Block { 100 | const request = { 101 | method: (bruRequest.http?.method || "POST").toUpperCase(), 102 | url: bruRequest.http?.url || bruRequest.meta?.url || "", 103 | httpVersion: "HTTP/1.1", 104 | headers: [] as Header[], 105 | body: null as string | null, 106 | }; 107 | 108 | const block: Block = { 109 | requestSeparator: { 110 | text: null, 111 | }, 112 | metadata: [], 113 | comments: [], 114 | request, 115 | preRequestScripts: [], 116 | postRequestScripts: [], 117 | responseRedirect: null, 118 | }; 119 | 120 | // Add folder path and name as comments 121 | if (folderPath) { 122 | block.comments.push(`# Folder: ${folderPath}\n`); 123 | } 124 | if (bruRequest.meta?.name) { 125 | block.comments.push(`# ${bruRequest.meta.name}\n`); 126 | block.metadata.push({ 127 | key: "name", 128 | value: bruRequest.meta.name.replace(/\s+/g, "_").toUpperCase(), 129 | }); 130 | } 131 | 132 | // Handle headers - they come as array of { name, value, enabled } 133 | if (Array.isArray(bruRequest.headers)) { 134 | bruRequest.headers 135 | .filter((header) => header.enabled !== false) 136 | .forEach((header) => { 137 | request.headers.push({ 138 | key: header.name, 139 | value: header.value, 140 | }); 141 | }); 142 | } 143 | 144 | // Handle GraphQL specific headers and body 145 | if (bruRequest.body?.graphql) { 146 | request.headers.push( 147 | { key: "Accept", value: "application/json" }, 148 | { key: "Content-Type", value: "application/json" }, 149 | { key: "X-REQUEST-TYPE", value: "GraphQL" }, 150 | ); 151 | 152 | const query = bruRequest.body.graphql.query; 153 | const variables = bruRequest.body.graphql.variables; 154 | 155 | if (query) { 156 | request.body = query; 157 | if (variables) { 158 | request.body += "\n\n" + variables; 159 | } 160 | } 161 | } else if (bruRequest.body?.json) { 162 | request.body = bruRequest.body.json; 163 | 164 | if ( 165 | !request.headers.some((h) => h.key.toLowerCase() === "content-type") 166 | ) { 167 | request.headers.push({ 168 | key: "Content-Type", 169 | value: "application/json", 170 | }); 171 | } 172 | } else if (bruRequest.body?.formUrlEncoded) { 173 | const formEntries = bruRequest.body.formUrlEncoded; 174 | const formParts: string[] = []; 175 | 176 | // Log the form data for debugging 177 | console.log("Form data:", formEntries); 178 | 179 | if (Array.isArray(formEntries)) { 180 | formEntries 181 | .filter((entry) => entry.enabled !== false) 182 | .forEach((entry) => { 183 | if ( 184 | entry.name && 185 | entry.value !== undefined && 186 | entry.value !== null 187 | ) { 188 | const value = String(entry.value); 189 | // Don't encode {{variables}} 190 | const encodedValue = value.match(/^{{.*}}$/) 191 | ? value 192 | : encodeURIComponent(value); 193 | 194 | formParts.push( 195 | `${encodeURIComponent(entry.name)}=${encodedValue}`, 196 | ); 197 | } 198 | }); 199 | } 200 | 201 | request.body = formParts.join("&"); 202 | 203 | request.headers.push({ 204 | key: "Content-Type", 205 | value: "application/x-www-form-urlencoded", 206 | }); 207 | } else if (bruRequest.body?.multipartForm) { 208 | const boundary = 209 | "----WebKitFormBoundary" + Math.random().toString(36).slice(2); 210 | const parts: string[] = []; 211 | 212 | bruRequest.body.multipartForm.forEach((field) => { 213 | parts.push( 214 | `--${boundary}\r\n` + 215 | `Content-Disposition: form-data; name="${field.name}"\r\n` + 216 | (field.type ? `Content-Type: ${field.type}\r\n` : "") + 217 | `\r\n${field.value}\r\n`, 218 | ); 219 | }); 220 | parts.push(`--${boundary}--\r\n`); 221 | 222 | request.body = parts.join(""); 223 | request.headers.push({ 224 | key: "Content-Type", 225 | value: `multipart/form-data; boundary=${boundary}`, 226 | }); 227 | } 228 | 229 | // Handle scripts 230 | if (bruRequest["script:pre-request"]) { 231 | block.preRequestScripts.push({ 232 | script: bruRequest["script:pre-request"].trim(), 233 | inline: true, 234 | }); 235 | } 236 | 237 | if (bruRequest.tests) { 238 | block.postRequestScripts.push({ 239 | script: bruRequest.tests.trim(), 240 | inline: true, 241 | }); 242 | } 243 | 244 | return block; 245 | } 246 | 247 | private processDirectory(dirPath: string): Document { 248 | const document: Document = { 249 | variables: [], 250 | blocks: [], 251 | }; 252 | 253 | const items = readdirSync(dirPath); 254 | 255 | for (const item of items) { 256 | const fullPath = join(dirPath, item); 257 | const stat = statSync(fullPath); 258 | 259 | // Skip the environments directory completely 260 | if (item === "environments") { 261 | continue; 262 | } 263 | 264 | if (stat.isDirectory()) { 265 | const subDocument = this.processDirectory(fullPath); 266 | document.blocks.push(...subDocument.blocks); 267 | document.variables.push(...subDocument.variables); 268 | } else if (item.endsWith(".bru")) { 269 | const content = readFileSync(fullPath, "utf-8"); 270 | const bruRequest = BrunoToJSONParser.parse(content) as BrunoRequest; 271 | const relativePath = relative(dirPath, fullPath).replace(".bru", ""); 272 | 273 | const block = this.buildRequestBlock(bruRequest, relativePath); 274 | document.blocks.push(block); 275 | } 276 | } 277 | 278 | return document; 279 | } 280 | 281 | private getEnvironments(dirPath: string): EnvironmentInfo[] { 282 | const environments: EnvironmentInfo[] = []; 283 | const environmentsDir = join(dirPath, "environments"); 284 | 285 | if (statSync(environmentsDir, { throwIfNoEntry: false })?.isDirectory()) { 286 | const envFiles = readdirSync(environmentsDir); 287 | 288 | for (const file of envFiles) { 289 | if (file.endsWith(".bru")) { 290 | const envContent = readFileSync(join(environmentsDir, file), "utf-8"); 291 | const environment = this.parseEnvironmentBruFile(envContent); 292 | 293 | environments.push({ 294 | name: file.replace(".bru", ""), 295 | vars: environment.vars || {}, 296 | }); 297 | } 298 | } 299 | } 300 | 301 | return environments; 302 | } 303 | 304 | private readCollectionInfo(dirPath: string): BrunoCollection | undefined { 305 | try { 306 | const brunoJsonPath = join(dirPath, "bruno.json"); 307 | const content = readFileSync(brunoJsonPath, "utf-8"); 308 | return JSON.parse(content) as BrunoCollection; 309 | } catch { 310 | return undefined; 311 | } 312 | } 313 | 314 | parse(collectionPath: string): ParseResult { 315 | // Read collection info first 316 | collectionInfo = this.readCollectionInfo(collectionPath); 317 | const environments = this.getEnvironments(collectionPath); 318 | 319 | // If no environments found, create a default document 320 | if (environments.length === 0) { 321 | return { 322 | documents: [this.processDirectory(collectionPath)], 323 | environmentNames: ["default"], 324 | collectionName: collectionInfo?.name || "bruno-collection", 325 | }; 326 | } 327 | 328 | // Create a document for each environment 329 | const documents = environments.map((env) => { 330 | const doc = this.processDirectory(collectionPath); 331 | // Add environment variables to the document 332 | doc.variables = Object.entries(env.vars).map( 333 | ([key, value]): Variable => ({ 334 | key, 335 | value, 336 | }), 337 | ); 338 | return doc; 339 | }); 340 | 341 | return { 342 | documents, 343 | environmentNames: environments.map((env) => env.name), 344 | collectionName: collectionInfo?.name || "bruno-collection", 345 | }; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/lib/parser/BrunoToJson.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | import * as ohm from "ohm-js"; 4 | import * as _ from "lodash"; 5 | const outdentString = (str: string) => { 6 | const match = str.match(/^[ \t]*(?=\S)/gm); 7 | if (!match) { 8 | return str; 9 | } 10 | const indent = Math.min(...match.map((el) => el.length)); 11 | const regexp = new RegExp(`^[ \\t]{${indent}}`, "gm"); 12 | return indent > 0 ? str.replace(regexp, "") : str; 13 | }; 14 | 15 | /** 16 | * A Bru file is made up of blocks. 17 | * There are two types of blocks 18 | * 19 | * 1. Dictionary Blocks - These are blocks that have key value pairs 20 | * ex: 21 | * headers { 22 | * content-type: application/json 23 | * } 24 | * 25 | * 2. Text Blocks - These are blocks that have text 26 | * ex: 27 | * body:json { 28 | * { 29 | * "username": "John Nash", 30 | * "password": "governingdynamics 31 | * } 32 | * 33 | */ 34 | const grammar = ohm.grammar(`Bru { 35 | BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* 36 | auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey 37 | bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body 38 | bodyforms = bodyformurlencoded | bodymultipart | bodyfile 39 | params = paramspath | paramsquery 40 | 41 | nl = "\\r"? "\\n" 42 | st = " " | "\\t" 43 | stnl = st | nl 44 | tagend = nl "}" 45 | optionalnl = ~tagend nl 46 | keychar = ~(tagend | st | nl | ":") any 47 | valuechar = ~(nl | tagend) any 48 | 49 | // Multiline text block surrounded by ''' 50 | multilinetextblockdelimiter = "'''" 51 | multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter 52 | 53 | // Dictionary Blocks 54 | dictionary = st* "{" pairlist? tagend 55 | pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)* 56 | pair = st* key st* ":" st* value st* 57 | key = keychar* 58 | value = multilinetextblock | valuechar* 59 | 60 | // Dictionary for Assert Block 61 | assertdictionary = st* "{" assertpairlist? tagend 62 | assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)* 63 | assertpair = st* assertkey st* ":" st* value st* 64 | assertkey = ~tagend assertkeychar* 65 | assertkeychar = ~(tagend | nl | ":") any 66 | 67 | // Text Blocks 68 | textblock = textline (~tagend nl textline)* 69 | textline = textchar* 70 | textchar = ~nl any 71 | 72 | meta = "meta" dictionary 73 | 74 | http = get | post | put | delete | patch | options | head | connect | trace 75 | get = "get" dictionary 76 | post = "post" dictionary 77 | put = "put" dictionary 78 | delete = "delete" dictionary 79 | patch = "patch" dictionary 80 | options = "options" dictionary 81 | head = "head" dictionary 82 | connect = "connect" dictionary 83 | trace = "trace" dictionary 84 | 85 | headers = "headers" dictionary 86 | 87 | query = "query" dictionary 88 | paramspath = "params:path" dictionary 89 | paramsquery = "params:query" dictionary 90 | 91 | varsandassert = varsreq | varsres | assert 92 | varsreq = "vars:pre-request" dictionary 93 | varsres = "vars:post-response" dictionary 94 | assert = "assert" assertdictionary 95 | 96 | authawsv4 = "auth:awsv4" dictionary 97 | authbasic = "auth:basic" dictionary 98 | authbearer = "auth:bearer" dictionary 99 | authdigest = "auth:digest" dictionary 100 | authNTLM = "auth:ntlm" dictionary 101 | authOAuth2 = "auth:oauth2" dictionary 102 | authwsse = "auth:wsse" dictionary 103 | authapikey = "auth:apikey" dictionary 104 | 105 | body = "body" st* "{" nl* textblock tagend 106 | bodyjson = "body:json" st* "{" nl* textblock tagend 107 | bodytext = "body:text" st* "{" nl* textblock tagend 108 | bodyxml = "body:xml" st* "{" nl* textblock tagend 109 | bodysparql = "body:sparql" st* "{" nl* textblock tagend 110 | bodygraphql = "body:graphql" st* "{" nl* textblock tagend 111 | bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend 112 | 113 | bodyformurlencoded = "body:form-urlencoded" dictionary 114 | bodymultipart = "body:multipart-form" dictionary 115 | bodyfile = "body:file" dictionary 116 | 117 | script = scriptreq | scriptres 118 | scriptreq = "script:pre-request" st* "{" nl* textblock tagend 119 | scriptres = "script:post-response" st* "{" nl* textblock tagend 120 | tests = "tests" st* "{" nl* textblock tagend 121 | docs = "docs" st* "{" nl* textblock tagend 122 | }`); 123 | 124 | const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { 125 | if (!pairList.length) { 126 | return []; 127 | } 128 | return _.map(pairList[0], (pair) => { 129 | let name = _.keys(pair)[0]; 130 | let value = pair[name]; 131 | 132 | if (!parseEnabled) { 133 | return { 134 | name, 135 | value, 136 | }; 137 | } 138 | 139 | let enabled = true; 140 | if (name && name.length && name.charAt(0) === "~") { 141 | name = name.slice(1); 142 | enabled = false; 143 | } 144 | 145 | return { 146 | name, 147 | value, 148 | enabled, 149 | }; 150 | }); 151 | }; 152 | 153 | const mapRequestParams = (pairList = [], type) => { 154 | if (!pairList.length) { 155 | return []; 156 | } 157 | return _.map(pairList[0], (pair) => { 158 | let name = _.keys(pair)[0]; 159 | let value = pair[name]; 160 | let enabled = true; 161 | if (name && name.length && name.charAt(0) === "~") { 162 | name = name.slice(1); 163 | enabled = false; 164 | } 165 | 166 | return { 167 | name, 168 | value, 169 | enabled, 170 | type, 171 | }; 172 | }); 173 | }; 174 | 175 | const multipartExtractContentType = (pair) => { 176 | if (_.isString(pair.value)) { 177 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); 178 | if (match != null && match.length > 2) { 179 | pair.value = match[1]; 180 | pair.contentType = match[2]; 181 | } else { 182 | pair.contentType = ""; 183 | } 184 | } 185 | }; 186 | 187 | const fileExtractContentType = (pair) => { 188 | if (_.isString(pair.value)) { 189 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); 190 | if (match && match.length > 2) { 191 | pair.value = match[1].trim(); 192 | pair.contentType = match[2].trim(); 193 | } else { 194 | pair.contentType = ""; 195 | } 196 | } 197 | }; 198 | 199 | const mapPairListToKeyValPairsMultipart = ( 200 | pairList = [], 201 | parseEnabled = true, 202 | ) => { 203 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); 204 | 205 | return pairs.map((pair) => { 206 | pair.type = "text"; 207 | multipartExtractContentType(pair); 208 | 209 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) { 210 | let filestr = pair.value.replace(/^@file\(/, "").replace(/\)$/, ""); 211 | pair.type = "file"; 212 | pair.value = filestr.split("|"); 213 | } 214 | 215 | return pair; 216 | }); 217 | }; 218 | 219 | const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => { 220 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); 221 | return pairs.map((pair) => { 222 | fileExtractContentType(pair); 223 | 224 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) { 225 | let filePath = pair.value.replace(/^@file\(/, "").replace(/\)$/, ""); 226 | pair.filePath = filePath; 227 | pair.selected = pair.enabled; 228 | 229 | // Remove pair.value as it only contains the file path reference 230 | delete pair.value; 231 | // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.) 232 | delete pair.name; 233 | delete pair.enabled; 234 | } 235 | 236 | return pair; 237 | }); 238 | }; 239 | 240 | const concatArrays = (objValue, srcValue) => { 241 | if (_.isArray(objValue) && _.isArray(srcValue)) { 242 | return objValue.concat(srcValue); 243 | } 244 | }; 245 | 246 | const mapPairListToKeyValPair = (pairList = []) => { 247 | if (!pairList || !pairList.length) { 248 | return {}; 249 | } 250 | 251 | return _.merge({}, ...pairList[0]); 252 | }; 253 | 254 | const sem = grammar.createSemantics().addAttribute("ast", { 255 | BruFile(tags) { 256 | if (!tags || !tags.ast || !tags.ast.length) { 257 | return {}; 258 | } 259 | 260 | return _.reduce( 261 | tags.ast, 262 | (result, item) => { 263 | return _.mergeWith(result, item, concatArrays); 264 | }, 265 | {}, 266 | ); 267 | }, 268 | dictionary(_1, _2, pairlist, _3) { 269 | return pairlist.ast; 270 | }, 271 | pairlist(_1, pair, _2, rest, _3) { 272 | return [pair.ast, ...rest.ast]; 273 | }, 274 | pair(_1, key, _2, _3, _4, value, _5) { 275 | let res = {}; 276 | res[key.ast] = value.ast ? value.ast.trim() : ""; 277 | return res; 278 | }, 279 | key(chars) { 280 | return chars.sourceString ? chars.sourceString.trim() : ""; 281 | }, 282 | value(chars) { 283 | try { 284 | let isMultiline = 285 | chars.sourceString?.startsWith(`'''`) && 286 | chars.sourceString?.endsWith(`'''`); 287 | if (isMultiline) { 288 | const multilineString = chars.sourceString?.replace(/^'''|'''$/g, ""); 289 | return multilineString 290 | .split("\n") 291 | .map((line) => line.slice(4)) 292 | .join("\n"); 293 | } 294 | return chars.sourceString ? chars.sourceString.trim() : ""; 295 | } catch (err) { 296 | console.error(err); 297 | } 298 | return chars.sourceString ? chars.sourceString.trim() : ""; 299 | }, 300 | assertdictionary(_1, _2, pairlist, _3) { 301 | return pairlist.ast; 302 | }, 303 | assertpairlist(_1, pair, _2, rest, _3) { 304 | return [pair.ast, ...rest.ast]; 305 | }, 306 | assertpair(_1, key, _2, _3, _4, value, _5) { 307 | let res = {}; 308 | res[key.ast] = value.ast ? value.ast.trim() : ""; 309 | return res; 310 | }, 311 | assertkey(chars) { 312 | return chars.sourceString ? chars.sourceString.trim() : ""; 313 | }, 314 | textblock(line, _1, rest) { 315 | return [line.ast, ...rest.ast].join("\n"); 316 | }, 317 | textline(chars) { 318 | return chars.sourceString; 319 | }, 320 | textchar(char) { 321 | return char.sourceString; 322 | }, 323 | nl(_1, _2) { 324 | return ""; 325 | }, 326 | st(_) { 327 | return ""; 328 | }, 329 | tagend(_1, _2) { 330 | return ""; 331 | }, 332 | _iter(...elements) { 333 | return elements.map((e) => e.ast); 334 | }, 335 | meta(_1, dictionary) { 336 | let meta = mapPairListToKeyValPair(dictionary.ast); 337 | 338 | if (!meta.seq) { 339 | meta.seq = 1; 340 | } 341 | 342 | if (!meta.type) { 343 | meta.type = "http"; 344 | } 345 | 346 | return { 347 | meta, 348 | }; 349 | }, 350 | get(_1, dictionary) { 351 | return { 352 | http: { 353 | method: "get", 354 | ...mapPairListToKeyValPair(dictionary.ast), 355 | }, 356 | }; 357 | }, 358 | post(_1, dictionary) { 359 | return { 360 | http: { 361 | method: "post", 362 | ...mapPairListToKeyValPair(dictionary.ast), 363 | }, 364 | }; 365 | }, 366 | put(_1, dictionary) { 367 | return { 368 | http: { 369 | method: "put", 370 | ...mapPairListToKeyValPair(dictionary.ast), 371 | }, 372 | }; 373 | }, 374 | delete(_1, dictionary) { 375 | return { 376 | http: { 377 | method: "delete", 378 | ...mapPairListToKeyValPair(dictionary.ast), 379 | }, 380 | }; 381 | }, 382 | patch(_1, dictionary) { 383 | return { 384 | http: { 385 | method: "patch", 386 | ...mapPairListToKeyValPair(dictionary.ast), 387 | }, 388 | }; 389 | }, 390 | options(_1, dictionary) { 391 | return { 392 | http: { 393 | method: "options", 394 | ...mapPairListToKeyValPair(dictionary.ast), 395 | }, 396 | }; 397 | }, 398 | head(_1, dictionary) { 399 | return { 400 | http: { 401 | method: "head", 402 | ...mapPairListToKeyValPair(dictionary.ast), 403 | }, 404 | }; 405 | }, 406 | connect(_1, dictionary) { 407 | return { 408 | http: { 409 | method: "connect", 410 | ...mapPairListToKeyValPair(dictionary.ast), 411 | }, 412 | }; 413 | }, 414 | query(_1, dictionary) { 415 | return { 416 | params: mapRequestParams(dictionary.ast, "query"), 417 | }; 418 | }, 419 | paramspath(_1, dictionary) { 420 | return { 421 | params: mapRequestParams(dictionary.ast, "path"), 422 | }; 423 | }, 424 | paramsquery(_1, dictionary) { 425 | return { 426 | params: mapRequestParams(dictionary.ast, "query"), 427 | }; 428 | }, 429 | headers(_1, dictionary) { 430 | return { 431 | headers: mapPairListToKeyValPairs(dictionary.ast), 432 | }; 433 | }, 434 | authawsv4(_1, dictionary) { 435 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 436 | const accessKeyIdKey = _.find(auth, { name: "accessKeyId" }); 437 | const secretAccessKeyKey = _.find(auth, { name: "secretAccessKey" }); 438 | const sessionTokenKey = _.find(auth, { name: "sessionToken" }); 439 | const serviceKey = _.find(auth, { name: "service" }); 440 | const regionKey = _.find(auth, { name: "region" }); 441 | const profileNameKey = _.find(auth, { name: "profileName" }); 442 | const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : ""; 443 | const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : ""; 444 | const sessionToken = sessionTokenKey ? sessionTokenKey.value : ""; 445 | const service = serviceKey ? serviceKey.value : ""; 446 | const region = regionKey ? regionKey.value : ""; 447 | const profileName = profileNameKey ? profileNameKey.value : ""; 448 | return { 449 | auth: { 450 | awsv4: { 451 | accessKeyId, 452 | secretAccessKey, 453 | sessionToken, 454 | service, 455 | region, 456 | profileName, 457 | }, 458 | }, 459 | }; 460 | }, 461 | authbasic(_1, dictionary) { 462 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 463 | const usernameKey = _.find(auth, { name: "username" }); 464 | const passwordKey = _.find(auth, { name: "password" }); 465 | const username = usernameKey ? usernameKey.value : ""; 466 | const password = passwordKey ? passwordKey.value : ""; 467 | return { 468 | auth: { 469 | basic: { 470 | username, 471 | password, 472 | }, 473 | }, 474 | }; 475 | }, 476 | authbearer(_1, dictionary) { 477 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 478 | const tokenKey = _.find(auth, { name: "token" }); 479 | const token = tokenKey ? tokenKey.value : ""; 480 | return { 481 | auth: { 482 | bearer: { 483 | token, 484 | }, 485 | }, 486 | }; 487 | }, 488 | authdigest(_1, dictionary) { 489 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 490 | const usernameKey = _.find(auth, { name: "username" }); 491 | const passwordKey = _.find(auth, { name: "password" }); 492 | const username = usernameKey ? usernameKey.value : ""; 493 | const password = passwordKey ? passwordKey.value : ""; 494 | return { 495 | auth: { 496 | digest: { 497 | username, 498 | password, 499 | }, 500 | }, 501 | }; 502 | }, 503 | authNTLM(_1, dictionary) { 504 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 505 | const usernameKey = _.find(auth, { name: "username" }); 506 | const passwordKey = _.find(auth, { name: "password" }); 507 | const domainKey = _.find(auth, { name: "domain" }); 508 | 509 | const username = usernameKey ? usernameKey.value : ""; 510 | const password = passwordKey ? passwordKey.value : ""; 511 | const domain = passwordKey ? domainKey.value : ""; 512 | 513 | return { 514 | auth: { 515 | ntlm: { 516 | username, 517 | password, 518 | domain, 519 | }, 520 | }, 521 | }; 522 | }, 523 | authOAuth2(_1, dictionary) { 524 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 525 | const grantTypeKey = _.find(auth, { name: "grant_type" }); 526 | const usernameKey = _.find(auth, { name: "username" }); 527 | const passwordKey = _.find(auth, { name: "password" }); 528 | const callbackUrlKey = _.find(auth, { name: "callback_url" }); 529 | const authorizationUrlKey = _.find(auth, { name: "authorization_url" }); 530 | const accessTokenUrlKey = _.find(auth, { name: "access_token_url" }); 531 | const clientIdKey = _.find(auth, { name: "client_id" }); 532 | const clientSecretKey = _.find(auth, { name: "client_secret" }); 533 | const scopeKey = _.find(auth, { name: "scope" }); 534 | const stateKey = _.find(auth, { name: "state" }); 535 | const pkceKey = _.find(auth, { name: "pkce" }); 536 | return { 537 | auth: { 538 | oauth2: 539 | grantTypeKey?.value && grantTypeKey?.value == "password" 540 | ? { 541 | grantType: grantTypeKey ? grantTypeKey.value : "", 542 | accessTokenUrl: accessTokenUrlKey 543 | ? accessTokenUrlKey.value 544 | : "", 545 | username: usernameKey ? usernameKey.value : "", 546 | password: passwordKey ? passwordKey.value : "", 547 | clientId: clientIdKey ? clientIdKey.value : "", 548 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 549 | scope: scopeKey ? scopeKey.value : "", 550 | } 551 | : grantTypeKey?.value && grantTypeKey?.value == "authorization_code" 552 | ? { 553 | grantType: grantTypeKey ? grantTypeKey.value : "", 554 | callbackUrl: callbackUrlKey ? callbackUrlKey.value : "", 555 | authorizationUrl: authorizationUrlKey 556 | ? authorizationUrlKey.value 557 | : "", 558 | accessTokenUrl: accessTokenUrlKey 559 | ? accessTokenUrlKey.value 560 | : "", 561 | clientId: clientIdKey ? clientIdKey.value : "", 562 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 563 | scope: scopeKey ? scopeKey.value : "", 564 | state: stateKey ? stateKey.value : "", 565 | pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false, 566 | } 567 | : grantTypeKey?.value && 568 | grantTypeKey?.value == "client_credentials" 569 | ? { 570 | grantType: grantTypeKey ? grantTypeKey.value : "", 571 | accessTokenUrl: accessTokenUrlKey 572 | ? accessTokenUrlKey.value 573 | : "", 574 | clientId: clientIdKey ? clientIdKey.value : "", 575 | clientSecret: clientSecretKey ? clientSecretKey.value : "", 576 | scope: scopeKey ? scopeKey.value : "", 577 | } 578 | : {}, 579 | }, 580 | }; 581 | }, 582 | authwsse(_1, dictionary) { 583 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 584 | 585 | const userKey = _.find(auth, { name: "username" }); 586 | const secretKey = _.find(auth, { name: "password" }); 587 | const username = userKey ? userKey.value : ""; 588 | const password = secretKey ? secretKey.value : ""; 589 | 590 | return { 591 | auth: { 592 | wsse: { 593 | username, 594 | password, 595 | }, 596 | }, 597 | }; 598 | }, 599 | authapikey(_1, dictionary) { 600 | const auth = mapPairListToKeyValPairs(dictionary.ast, false); 601 | 602 | const findValueByName = (name) => { 603 | const item = _.find(auth, { name }); 604 | return item ? item.value : ""; 605 | }; 606 | 607 | const key = findValueByName("key"); 608 | const value = findValueByName("value"); 609 | const placement = findValueByName("placement"); 610 | 611 | return { 612 | auth: { 613 | apikey: { 614 | key, 615 | value, 616 | placement, 617 | }, 618 | }, 619 | }; 620 | }, 621 | bodyformurlencoded(_1, dictionary) { 622 | return { 623 | body: { 624 | formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast), 625 | }, 626 | }; 627 | }, 628 | bodymultipart(_1, dictionary) { 629 | return { 630 | body: { 631 | multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast), 632 | }, 633 | }; 634 | }, 635 | bodyfile(_1, dictionary) { 636 | return { 637 | body: { 638 | file: mapPairListToKeyValPairsFile(dictionary.ast), 639 | }, 640 | }; 641 | }, 642 | body(_1, _2, _3, _4, textblock, _5) { 643 | return { 644 | http: { 645 | body: "json", 646 | }, 647 | body: { 648 | json: outdentString(textblock.sourceString), 649 | }, 650 | }; 651 | }, 652 | bodyjson(_1, _2, _3, _4, textblock, _5) { 653 | return { 654 | body: { 655 | json: outdentString(textblock.sourceString), 656 | }, 657 | }; 658 | }, 659 | bodytext(_1, _2, _3, _4, textblock, _5) { 660 | return { 661 | body: { 662 | text: outdentString(textblock.sourceString), 663 | }, 664 | }; 665 | }, 666 | bodyxml(_1, _2, _3, _4, textblock, _5) { 667 | return { 668 | body: { 669 | xml: outdentString(textblock.sourceString), 670 | }, 671 | }; 672 | }, 673 | bodysparql(_1, _2, _3, _4, textblock, _5) { 674 | return { 675 | body: { 676 | sparql: outdentString(textblock.sourceString), 677 | }, 678 | }; 679 | }, 680 | bodygraphql(_1, _2, _3, _4, textblock, _5) { 681 | return { 682 | body: { 683 | graphql: { 684 | query: outdentString(textblock.sourceString), 685 | }, 686 | }, 687 | }; 688 | }, 689 | bodygraphqlvars(_1, _2, _3, _4, textblock, _5) { 690 | return { 691 | body: { 692 | graphql: { 693 | variables: outdentString(textblock.sourceString), 694 | }, 695 | }, 696 | }; 697 | }, 698 | varsreq(_1, dictionary) { 699 | const vars = mapPairListToKeyValPairs(dictionary.ast); 700 | _.each(vars, (v) => { 701 | let name = v.name; 702 | if (name && name.length && name.charAt(0) === "@") { 703 | v.name = name.slice(1); 704 | v.local = true; 705 | } else { 706 | v.local = false; 707 | } 708 | }); 709 | 710 | return { 711 | vars: { 712 | req: vars, 713 | }, 714 | }; 715 | }, 716 | varsres(_1, dictionary) { 717 | const vars = mapPairListToKeyValPairs(dictionary.ast); 718 | _.each(vars, (v) => { 719 | let name = v.name; 720 | if (name && name.length && name.charAt(0) === "@") { 721 | v.name = name.slice(1); 722 | v.local = true; 723 | } else { 724 | v.local = false; 725 | } 726 | }); 727 | 728 | return { 729 | vars: { 730 | res: vars, 731 | }, 732 | }; 733 | }, 734 | assert(_1, dictionary) { 735 | return { 736 | assertions: mapPairListToKeyValPairs(dictionary.ast), 737 | }; 738 | }, 739 | scriptreq(_1, _2, _3, _4, textblock, _5) { 740 | return { 741 | script: { 742 | req: outdentString(textblock.sourceString), 743 | }, 744 | }; 745 | }, 746 | scriptres(_1, _2, _3, _4, textblock, _5) { 747 | return { 748 | script: { 749 | res: outdentString(textblock.sourceString), 750 | }, 751 | }; 752 | }, 753 | tests(_1, _2, _3, _4, textblock, _5) { 754 | return { 755 | tests: outdentString(textblock.sourceString), 756 | }; 757 | }, 758 | docs(_1, _2, _3, _4, textblock, _5) { 759 | return { 760 | docs: outdentString(textblock.sourceString), 761 | }; 762 | }, 763 | }); 764 | 765 | const parser = (input) => { 766 | const match = grammar.match(input); 767 | 768 | if (match.succeeded()) { 769 | return sem(match).ast; 770 | } else { 771 | throw new Error(match.message); 772 | } 773 | }; 774 | 775 | export const BrunoToJSONParser = { 776 | parse: (input) => { 777 | return parser(input); 778 | }, 779 | }; 780 | -------------------------------------------------------------------------------- /src/lib/parser/Diff.ts: -------------------------------------------------------------------------------- 1 | import "colors"; 2 | import * as diff from "diff"; 3 | 4 | export const Diff = (build: string, content: string) => { 5 | const d = diff.diffChars(content, build); 6 | 7 | d.forEach((part) => { 8 | // green for additions, red for deletions 9 | // with a black color 10 | const text = part.added 11 | ? part.value.bgGreen 12 | : part.removed 13 | ? part.value.bgRed 14 | : part.value; 15 | process.stderr.write(text); 16 | }); 17 | 18 | console.log(); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/parser/DocumentBuilder.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import type { Document, Block, Header } from "./DocumentParser"; 3 | 4 | function headerToPascalCase(str: string) { 5 | return str 6 | .split("-") 7 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 8 | .join("-"); 9 | } 10 | 11 | const formatFormBody = (body: string): string => { 12 | body = body.replace(/\n/g, "").replace(/\s+/g, ""); 13 | const parts = body.split("&"); 14 | if (parts.length === 1) { 15 | return parts[0]; 16 | } 17 | return parts.join("&\n").replace(/\n\s*\n/g, "\n"); 18 | }; 19 | 20 | // Get header value based on case insensitive key 21 | function getHeader(headers: Header[], key: string) { 22 | const header = headers.find( 23 | (header) => header.key.toLowerCase() === key.toLowerCase(), 24 | ); 25 | return header?.value.toLowerCase(); 26 | } 27 | 28 | // Helper function to split GraphQL body and variables 29 | function splitGraphQLBody(body: string): { 30 | query: string; 31 | variables: string | null; 32 | } { 33 | // Split on first occurrence of two newlines followed by a JSON-like structure 34 | const parts = body.split(/\n\s*\n(\s*{)/); 35 | 36 | if (parts.length >= 2) { 37 | // Rejoin the JSON part if it was split 38 | const variables = parts.slice(1).join(""); 39 | return { 40 | query: parts[0].trim(), 41 | variables: variables.trim(), 42 | }; 43 | } 44 | 45 | return { 46 | query: body.trim(), 47 | variables: null, 48 | }; 49 | } 50 | 51 | const getFormatParser = (block: Block): null | "graphql" | "json" => { 52 | const headers = block.request?.headers || []; 53 | if (getHeader(headers, "x-request-type") === "graphql") { 54 | return "graphql"; 55 | } 56 | if (getHeader(headers, "content-type") === "application/json") { 57 | return "json"; 58 | } 59 | return null; 60 | }; 61 | 62 | function replaceCommentPrefix(comment: string): string { 63 | return comment.replace(/^(\/\/)/, "#"); 64 | } 65 | 66 | function preservePlaceholders(body: string) { 67 | const placeholderRegex = /(?(); 69 | let replacedBody = body; 70 | 71 | replacedBody = replacedBody.replace(placeholderRegex, (match) => { 72 | const key = `__KULALA_FMT_PLACEHOLDER_${placeholders.size}__`; 73 | placeholders.set(key, match); 74 | return `"${key}"`; 75 | }); 76 | replacedBody = replacedBody.replace( 77 | /__""__KULALA_FMT_PLACEHOLDER_/g, 78 | "____KULALA_FMT_PLACEHOLDER_", 79 | ); 80 | 81 | return { replacedBody, placeholders }; 82 | } 83 | 84 | function restorePlaceholders( 85 | formattedBody: string, 86 | placeholders: Map, 87 | ) { 88 | let restoredBody = formattedBody; 89 | const placeholdersLength = placeholders.size; 90 | let idx = 0; 91 | placeholders.forEach((original, key) => { 92 | let firstQuote = ""; 93 | let lastQuote = ""; 94 | if (idx === 0) { 95 | firstQuote = '"'; 96 | } 97 | if (idx === placeholdersLength - 1) { 98 | lastQuote = '"'; 99 | } 100 | restoredBody = restoredBody.replace( 101 | `${firstQuote}${key}${lastQuote}`, 102 | original, 103 | ); 104 | idx++; 105 | }); 106 | return restoredBody; 107 | } 108 | 109 | const build = async ( 110 | document: Document, 111 | formatBody: boolean = true, 112 | ): Promise => { 113 | let output = ""; 114 | 115 | for (const variable of document.variables) { 116 | output += `@${variable.key} = ${variable.value}\n`; 117 | } 118 | output += "\n"; 119 | 120 | for (const block of document.blocks) { 121 | const requestSeparatorText = block.requestSeparator.text 122 | ? ` ${block.requestSeparator.text}` 123 | : ""; 124 | output += `\n###${requestSeparatorText}\n\n`; 125 | 126 | if (block.comments.length > 0) { 127 | for (const comment of block.comments) { 128 | output += `${replaceCommentPrefix(comment)}`; 129 | } 130 | } 131 | if (block.preRequestScripts.length > 0) { 132 | for (const script of block.preRequestScripts) { 133 | output += `< ${script.script}\n`; 134 | } 135 | } 136 | if (block.metadata.length > 0) { 137 | for (const metadata of block.metadata) { 138 | output += `# @${metadata.key} ${metadata.value}\n`; 139 | } 140 | } 141 | if (block.request) { 142 | const formatParser = getFormatParser(block); 143 | output += `${block.request.method} ${block.request.url} ${block.request.httpVersion}\n`; 144 | for (const header of block.request.headers) { 145 | let headerKey = header.key; 146 | switch (block.request.httpVersion) { 147 | case "HTTP/1.0": 148 | case "HTTP/1.1": 149 | headerKey = headerToPascalCase(header.key); 150 | break; 151 | case "HTTP/2": 152 | case "HTTP/3": 153 | headerKey = header.key.toLowerCase(); 154 | break; 155 | } 156 | output += `${headerKey}: ${header.value}\n`; 157 | } 158 | if (block.request.body) { 159 | let body = block.request.body.trim(); 160 | if (formatBody) { 161 | if (formatParser === "graphql") { 162 | try { 163 | const { replacedBody, placeholders } = preservePlaceholders(body); 164 | const { query, variables } = splitGraphQLBody(replacedBody); 165 | 166 | const formattedQuery = await prettier.format(query, { 167 | parser: formatParser, 168 | }); 169 | 170 | // Format the variables if they exist 171 | let formattedVariables = ""; 172 | if (variables) { 173 | formattedVariables = await prettier.format(variables, { 174 | parser: "json", 175 | }); 176 | } 177 | body = formattedQuery.trim(); 178 | if (formattedVariables) { 179 | body += `\n\n${formattedVariables.trim()}`; 180 | } 181 | body = restorePlaceholders(body, placeholders).trim(); 182 | } catch (err) { 183 | const error = err as Error; 184 | console.log(error.message); 185 | process.exit(1); 186 | } 187 | } else if (formatParser === "json") { 188 | try { 189 | const { replacedBody, placeholders } = preservePlaceholders(body); 190 | 191 | body = await prettier.format(replacedBody, { 192 | parser: formatParser, 193 | }); 194 | 195 | body = restorePlaceholders(body, placeholders).trim(); 196 | } catch (err) { 197 | const error = err as Error; 198 | console.log(error.message); 199 | process.exit(1); 200 | } 201 | } else if ( 202 | getHeader(block.request.headers, "content-type") === 203 | "application/x-www-form-urlencoded" 204 | ) { 205 | body = formatFormBody(body); 206 | } 207 | } 208 | output += `\n${body}\n`; 209 | } 210 | } 211 | if (block.postRequestScripts.length > 0) { 212 | for (const script of block.postRequestScripts) { 213 | output += `\n> ${script.script}\n`; 214 | } 215 | } 216 | if (block.responseRedirect) { 217 | output += `\n${block.responseRedirect.trim()}\n`; 218 | } 219 | } 220 | output = output.trim() + "\n"; 221 | return output; 222 | }; 223 | 224 | export const DocumentBuilder = { 225 | build, 226 | }; 227 | -------------------------------------------------------------------------------- /src/lib/parser/DocumentParser.ts: -------------------------------------------------------------------------------- 1 | import Parser, { SyntaxNode, type Language } from "tree-sitter"; 2 | import Kulala from "@mistweaverco/tree-sitter-kulala"; 3 | import { configparser } from "./../configparser"; 4 | 5 | const config = configparser.parse(); 6 | 7 | export interface Header { 8 | key: string; 9 | value: string; 10 | } 11 | 12 | interface Request { 13 | method: string; 14 | url: string; 15 | httpVersion: string; 16 | headers: Header[]; 17 | body: string | null; 18 | } 19 | 20 | interface Metadata { 21 | key: string; 22 | value: string; 23 | } 24 | 25 | interface PreRequestScript { 26 | script: string; 27 | inline: boolean; 28 | } 29 | 30 | interface PostRequestScript { 31 | script: string; 32 | inline: boolean; 33 | } 34 | 35 | export interface Variable { 36 | key: string; 37 | value: string; 38 | } 39 | 40 | export interface BlockRequestSeparator { 41 | text: string | null; 42 | } 43 | 44 | export interface Block { 45 | requestSeparator: BlockRequestSeparator; 46 | metadata: Metadata[]; 47 | comments: string[]; 48 | request: Request | null; 49 | preRequestScripts: PreRequestScript[]; 50 | postRequestScripts: PostRequestScript[]; 51 | responseRedirect: string | null; 52 | } 53 | 54 | export interface Document { 55 | variables: Variable[]; 56 | blocks: Block[]; 57 | } 58 | 59 | const traverseNodes = ( 60 | node: SyntaxNode, 61 | callback: (node: SyntaxNode) => void, 62 | ): void => { 63 | callback(node); 64 | node.children.forEach((child) => traverseNodes(child, callback)); 65 | }; 66 | 67 | const documentHasErrors = (node: SyntaxNode): boolean => { 68 | const recursiveNodeWalk = ( 69 | node: SyntaxNode, 70 | callback: (node: SyntaxNode) => boolean, 71 | ): boolean => { 72 | if (callback(node)) { 73 | return true; // Return true if the callback returns true 74 | } 75 | for (const child of node.children) { 76 | if (recursiveNodeWalk(child, callback)) { 77 | return true; // Return true if a child node has an error 78 | } 79 | } 80 | return false; // Return false if no error is found in this subtree 81 | }; 82 | return recursiveNodeWalk(node, (n) => { 83 | return n.hasError; // Return true if the node has an error 84 | }); 85 | }; 86 | 87 | // formatUrl takes a URL string and retuns an formatted string 88 | // with the URL parts separated by new lines 89 | // For example, given the URL `https://httpbin.org/get?foo=bar&baz=qux` 90 | // the function should return: 91 | // ``` 92 | // https://httpbin.org/get 93 | // ?foo=bar 94 | // &baz=qux 95 | // ``` 96 | const formatUrl = (url: string): string => { 97 | const parts = url.split("?"); 98 | if (parts.length === 1) { 99 | return parts[0]; 100 | } 101 | const [base, query] = parts; 102 | return `${base}?${query.split("&").join("\n &")}`.replace(/\n\s*\n/g, "\n"); 103 | }; 104 | 105 | // Returns a Document object if the content is valid .http syntax, 106 | // otherwise null. 107 | const parse = (content: string): Document | null => { 108 | const parser = new Parser(); 109 | const language = Kulala as Language; 110 | parser.setLanguage(language); 111 | 112 | const tree = parser.parse(content); 113 | const documentNode = tree.rootNode; 114 | 115 | if (documentHasErrors(documentNode)) { 116 | return null; 117 | } 118 | 119 | const variables: Variable[] = []; 120 | const blocks: Block[] = []; 121 | 122 | const blockNodes = documentNode.children.filter( 123 | (node) => node.type === "section", 124 | ); 125 | 126 | blockNodes.forEach((bn) => { 127 | const block: Block = { 128 | requestSeparator: { 129 | text: null, 130 | }, 131 | metadata: [], 132 | comments: [], 133 | request: null, 134 | preRequestScripts: [], 135 | postRequestScripts: [], 136 | responseRedirect: null, 137 | }; 138 | 139 | const headers: Header[] = []; 140 | let method: string = ""; 141 | let url: string = ""; 142 | let httpVersion: string = ""; 143 | let body: string | null = null; 144 | 145 | bn.children.forEach((node) => { 146 | if (node.type === "pre_request_script") { 147 | let preRequestScript: PreRequestScript | null = null; 148 | node.children.forEach((child) => { 149 | switch (child.type) { 150 | case "script": 151 | preRequestScript = { 152 | script: child.text, 153 | inline: true, 154 | }; 155 | break; 156 | case "path": 157 | preRequestScript = { 158 | script: child.text, 159 | inline: false, 160 | }; 161 | break; 162 | } 163 | }); 164 | if (preRequestScript) { 165 | block.preRequestScripts.push(preRequestScript); 166 | } 167 | } 168 | if (node.type === "request_separator") { 169 | // without text 170 | if (node.children.length === 0) { 171 | block.requestSeparator.text = null; 172 | } 173 | node.children.forEach((child) => { 174 | switch (child.type) { 175 | case "value": 176 | block.requestSeparator.text = child.text; 177 | break; 178 | } 179 | }); 180 | } 181 | if (node.type === "comment") { 182 | // normal comment 183 | if (node.children.length === 0) { 184 | block.comments.push(node.text); 185 | } 186 | let md: Metadata | null = null; 187 | // metadata comments like `# @key value` 188 | node.children.forEach((child) => { 189 | switch (child.type) { 190 | case "identifier": 191 | md = { 192 | key: child.text, 193 | value: "", 194 | }; 195 | break; 196 | case "value": 197 | if (md) { 198 | md.value = child.text; 199 | } 200 | break; 201 | } 202 | }); 203 | if (md) { 204 | block.metadata.push(md); 205 | } 206 | } 207 | 208 | if (node.type === "request") { 209 | node.children.forEach((child) => { 210 | let postRequestScript: PostRequestScript | null = null; 211 | let parts, key, value; 212 | switch (child.type) { 213 | case "method": 214 | method = child.text.toUpperCase(); 215 | break; 216 | case "target_url": 217 | url = formatUrl(child.text); 218 | break; 219 | case "http_version": 220 | httpVersion = child.text; 221 | break; 222 | case "header": 223 | // a header string is in the format `key: value` 224 | // for example, `Content-Type: application/json` 225 | parts = child.text.split(":"); 226 | key = parts[0].trim(); 227 | // also make sure to take all parts after the first colon 228 | value = parts.slice(1).join(":").trim(); 229 | headers.push({ key, value }); 230 | break; 231 | case "res_redirect": 232 | block.responseRedirect = child.text; 233 | break; 234 | case "res_handler_script": 235 | child.children.forEach((c) => { 236 | switch (c.type) { 237 | case "script": 238 | postRequestScript = { 239 | script: c.text, 240 | inline: true, 241 | }; 242 | break; 243 | case "path": 244 | postRequestScript = { 245 | script: c.text, 246 | inline: false, 247 | }; 248 | break; 249 | } 250 | }); 251 | if (postRequestScript) { 252 | block.postRequestScripts.push(postRequestScript); 253 | } 254 | break; 255 | } 256 | if (child.type.endsWith("_body")) { 257 | body = child.text; 258 | } 259 | }); 260 | 261 | if (method === "") { 262 | method = config.defaults.http_method; 263 | } 264 | 265 | if (httpVersion === "") { 266 | httpVersion = config.defaults.http_version; 267 | } 268 | } 269 | block.request = { 270 | method, 271 | url, 272 | httpVersion, 273 | headers, 274 | body, 275 | }; 276 | }); 277 | if ( 278 | block.request?.url.length || 279 | block.metadata.length > 0 || 280 | block.comments.length > 0 281 | ) { 282 | // sort headers by key 283 | block.request?.headers.sort((a, b) => { 284 | return a.key.localeCompare(b.key); 285 | }); 286 | // sort metadata by key 287 | block.metadata.sort((a, b) => { 288 | return a.key.localeCompare(b.key); 289 | }); 290 | blocks.push(block); 291 | } 292 | }); 293 | 294 | traverseNodes(documentNode, (node) => { 295 | if (node.type === "variable_declaration") { 296 | let variable: Variable | null = null; 297 | // variables like `@key = value` 298 | node.children.forEach((child) => { 299 | switch (child.type) { 300 | case "identifier": 301 | variable = { 302 | key: child.text, 303 | value: "", 304 | }; 305 | break; 306 | case "value": 307 | if (variable) { 308 | variable.value = child.text; 309 | } 310 | break; 311 | } 312 | }); 313 | if (variable) { 314 | variables.push(variable); 315 | } 316 | } 317 | }); 318 | 319 | // sort variables by key 320 | variables.sort((a, b) => { 321 | return a.key.localeCompare(b.key); 322 | }); 323 | 324 | return { blocks, variables }; 325 | }; 326 | 327 | export const DocumentParser = { 328 | parse, 329 | }; 330 | -------------------------------------------------------------------------------- /src/lib/parser/OpenAPIDocumentParser.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Block, Header } from "./DocumentParser"; 2 | 3 | interface OpenAPIServer { 4 | url: string; 5 | description?: string; 6 | variables?: Record< 7 | string, 8 | { 9 | default: string; 10 | description?: string; 11 | enum?: string[]; 12 | } 13 | >; 14 | } 15 | 16 | interface OpenAPIParameter { 17 | name: string; 18 | in: "query" | "header" | "path" | "cookie"; 19 | description?: string; 20 | required?: boolean; 21 | deprecated?: boolean; 22 | example?: string | number | boolean; 23 | schema?: OpenAPISchema; 24 | } 25 | 26 | interface OpenAPISchema { 27 | type: "string" | "number" | "integer" | "boolean" | "array" | "object"; 28 | format?: string; 29 | properties?: Record; 30 | items?: OpenAPISchema; 31 | required?: string[]; 32 | description?: string; 33 | example?: OpenAPISchemaExample; 34 | } 35 | 36 | type OpenAPISchemaExample = 37 | | string 38 | | number 39 | | boolean 40 | | null 41 | | OpenAPISchemaExample[] 42 | | { [key: string]: OpenAPISchemaExample }; 43 | 44 | interface OpenAPIRequestBody { 45 | description?: string; 46 | required?: boolean; 47 | content: Record< 48 | string, 49 | { 50 | schema?: OpenAPISchema; 51 | example?: OpenAPISchemaExample; 52 | } 53 | >; 54 | } 55 | 56 | interface OpenAPIOperation { 57 | summary?: string; 58 | description?: string; 59 | operationId?: string; 60 | parameters?: OpenAPIParameter[]; 61 | requestBody?: OpenAPIRequestBody; 62 | responses?: Record< 63 | string, 64 | { 65 | description: string; 66 | content?: Record< 67 | string, 68 | { 69 | schema: OpenAPISchema; 70 | example?: OpenAPISchemaExample; 71 | } 72 | >; 73 | } 74 | >; 75 | } 76 | 77 | interface OpenAPIPathItem { 78 | get?: OpenAPIOperation; 79 | post?: OpenAPIOperation; 80 | put?: OpenAPIOperation; 81 | delete?: OpenAPIOperation; 82 | patch?: OpenAPIOperation; 83 | parameters?: OpenAPIParameter[]; 84 | } 85 | 86 | export interface OpenAPISpec { 87 | openapi: string; 88 | info: { 89 | title: string; 90 | version: string; 91 | description?: string; 92 | }; 93 | servers?: OpenAPIServer[]; 94 | paths: Record; 95 | components?: { 96 | schemas?: Record; 97 | parameters?: Record; 98 | requestBodies?: Record; 99 | responses?: Record; 100 | }; 101 | } 102 | 103 | interface OpenAPIParser { 104 | parse(openAPISpec: OpenAPISpec): ParseResult; 105 | } 106 | 107 | interface ParseResult { 108 | documents: Document[]; 109 | serverUrls: string[]; 110 | } 111 | 112 | export class OpenAPIDocumentParser implements OpenAPIParser { 113 | private buildRequestBlock( 114 | path: string, 115 | method: string, 116 | operation: OpenAPIOperation, 117 | ): Block { 118 | const block: Block = { 119 | requestSeparator: { 120 | text: null, 121 | }, 122 | metadata: [], 123 | comments: [], 124 | request: { 125 | method: method.toUpperCase(), 126 | url: path, 127 | httpVersion: "HTTP/1.1", 128 | headers: [], 129 | body: null, 130 | }, 131 | preRequestScripts: [], 132 | postRequestScripts: [], 133 | responseRedirect: null, 134 | }; 135 | 136 | // Add operation summary/description as comments 137 | if (operation.summary) { 138 | block.comments.push( 139 | operation.summary 140 | .split("\n") 141 | .map((l) => `# ${l}`) 142 | .join("\n") + "\n", 143 | ); 144 | } 145 | if (operation.description) { 146 | block.comments.push( 147 | operation.description 148 | .split("\n") 149 | .map((l) => `# ${l}`) 150 | .join("\n") + "\n", 151 | ); 152 | } 153 | 154 | // Add operationId as metadata 155 | if (operation.operationId) { 156 | block.metadata.push({ 157 | key: "name", 158 | value: operation.operationId, 159 | }); 160 | } 161 | 162 | // Since we know block.request is initialized above, we can assert it's non-null 163 | if (operation.requestBody?.content) { 164 | const content = operation.requestBody.content; 165 | const contentType = Object.keys(content)[0]; 166 | 167 | if (block.request) { 168 | // TypeScript guard 169 | block.request.headers.push({ 170 | key: "Content-Type", 171 | value: contentType, 172 | }); 173 | 174 | const mediaTypeObject = content[contentType]; 175 | if (mediaTypeObject.example) { 176 | block.request.body = JSON.stringify(mediaTypeObject.example, null, 2); 177 | } else if (mediaTypeObject.schema) { 178 | block.request.body = this.generateExampleFromSchema( 179 | mediaTypeObject.schema, 180 | ); 181 | } 182 | } 183 | } 184 | 185 | // Handle parameters 186 | if (operation.parameters && block.request) { 187 | // TypeScript guard 188 | for (const param of operation.parameters) { 189 | if (param.in === "header") { 190 | const header: Header = { 191 | key: param.name, 192 | value: param.example?.toString() || `{{${param.name}}}`, 193 | }; 194 | block.request.headers.push(header); 195 | } 196 | } 197 | } 198 | 199 | return block; 200 | } 201 | 202 | private generateExampleFromSchema(schema: OpenAPISchema): string { 203 | const example: Record = {}; 204 | if (schema.properties) { 205 | for (const [key, prop] of Object.entries(schema.properties)) { 206 | example[key] = prop.example || this.getDefaultValueForType(prop.type); 207 | } 208 | } 209 | return JSON.stringify(example, null, 2); 210 | } 211 | 212 | private getDefaultValueForType(type: OpenAPISchema["type"]): unknown { 213 | switch (type) { 214 | case "string": 215 | return "string"; 216 | case "number": 217 | case "integer": 218 | return 0; 219 | case "boolean": 220 | return false; 221 | case "array": 222 | return []; 223 | case "object": 224 | return {}; 225 | default: 226 | return null; 227 | } 228 | } 229 | 230 | private extractServerIdentifier(server: OpenAPIServer): string { 231 | // Remove protocol prefix (http:// or https://) 232 | let identifier = server.url.replace(/^https?:\/\//, ""); 233 | 234 | // Replace variable patterns with their default values or placeholder 235 | if (server.variables) { 236 | Object.entries(server.variables).forEach(([key, variable]) => { 237 | identifier = identifier.replace( 238 | `{${key}}`, 239 | variable.default || `[${key}]`, 240 | ); 241 | }); 242 | } 243 | 244 | // Remove port and path for cleaner identifier 245 | identifier = identifier.split(":")[0].split("/")[0]; 246 | 247 | return identifier; 248 | } 249 | 250 | parse(openAPISpec: OpenAPISpec): ParseResult { 251 | // If no servers are specified, create a single document with relative URLs 252 | if (!openAPISpec.servers || openAPISpec.servers.length === 0) { 253 | return { 254 | documents: [this.createDocument(openAPISpec)], 255 | serverUrls: ["default"], 256 | }; 257 | } 258 | 259 | // Create a document for each server 260 | const documents = openAPISpec.servers.map((server, index) => 261 | this.createDocument(openAPISpec, server, index), 262 | ); 263 | 264 | // Extract server identifiers in matching order 265 | const serverUrls = openAPISpec.servers.map((server) => 266 | this.extractServerIdentifier(server), 267 | ); 268 | 269 | return { 270 | documents, 271 | serverUrls, 272 | }; 273 | } 274 | 275 | private buildFullUrl( 276 | path: string, 277 | server?: OpenAPIServer, 278 | serverIndex?: number, 279 | ): string { 280 | if (!server) { 281 | return path; 282 | } 283 | 284 | // Remove leading slash from path 285 | const cleanPath = path.replace(/^\//, ""); 286 | 287 | let url = server.url; 288 | 289 | // Replace server variables with double curly brace format 290 | if (server.variables) { 291 | Object.keys(server.variables).forEach((key) => { 292 | url = url.replace(`{${key}}`, `{{${key}}}`); 293 | }); 294 | } 295 | 296 | // Replace the entire server URL with baseUrl variable 297 | url = `{{baseUrl${serverIndex || ""}}}/${cleanPath}`; 298 | 299 | return url; 300 | } 301 | 302 | private createDocument( 303 | openAPISpec: OpenAPISpec, 304 | server?: OpenAPIServer, 305 | serverIndex?: number, 306 | ): Document { 307 | const document: Document = { 308 | variables: [], 309 | blocks: [], 310 | }; 311 | 312 | // Add server URL as a variable if it exists 313 | if (server) { 314 | let serverUrl = server.url.replace(/\/$/, ""); // Remove trailing slash 315 | 316 | // Replace server variables with double curly braces in the URL 317 | if (server.variables) { 318 | Object.keys(server.variables).forEach((key) => { 319 | serverUrl = serverUrl.replace(`{${key}}`, `{{${key}}}`); 320 | }); 321 | } 322 | 323 | document.variables.push({ 324 | key: `baseUrl${serverIndex || ""}`, 325 | value: serverUrl, 326 | }); 327 | 328 | // Add server variables if they exist 329 | if (server.variables) { 330 | Object.entries(server.variables).forEach(([key, variable]) => { 331 | document.variables.push({ 332 | key: key, 333 | value: variable.default, 334 | }); 335 | }); 336 | } 337 | } 338 | 339 | // Parse paths into blocks 340 | for (const [path, pathItem] of Object.entries(openAPISpec.paths)) { 341 | for (const [method, operation] of Object.entries(pathItem)) { 342 | if (["get", "post", "put", "delete", "patch"].includes(method)) { 343 | const block = this.buildRequestBlock( 344 | this.buildFullUrl(path, server, serverIndex), 345 | method, 346 | operation as OpenAPIOperation, 347 | ); 348 | 349 | // Add server description as a comment if it exists 350 | if (server?.description) { 351 | block.comments.unshift(`# Server: ${server.description}\n`); 352 | } 353 | 354 | document.blocks.push(block); 355 | } 356 | } 357 | } 358 | 359 | return document; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/lib/parser/PostmanDocumentParser.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Block } from "./DocumentParser"; 2 | 3 | interface PostmanVariable { 4 | key: string; 5 | value: string; 6 | enabled?: boolean; 7 | type?: string; 8 | } 9 | 10 | interface PostmanHeader { 11 | key: string; 12 | value: string; 13 | disabled?: boolean; 14 | } 15 | 16 | interface PostmanRequest { 17 | method: string; 18 | header: PostmanHeader[]; 19 | url: { 20 | raw: string; 21 | protocol?: string; 22 | host?: string[]; 23 | path?: string[]; 24 | query?: Array<{ 25 | key: string; 26 | value: string; 27 | disabled?: boolean; 28 | }>; 29 | }; 30 | body?: { 31 | mode: "raw" | "formdata" | "urlencoded" | "file" | "graphql"; 32 | raw?: string; 33 | options?: { 34 | raw?: { 35 | language: string; 36 | }; 37 | }; 38 | }; 39 | description?: string; 40 | name?: string; 41 | } 42 | 43 | interface PostmanItem { 44 | name: string; 45 | request: PostmanRequest; 46 | response: unknown[]; 47 | } 48 | 49 | interface PostmanItemGroup { 50 | name: string; 51 | item: (PostmanItem | PostmanItemGroup)[]; 52 | description?: string; 53 | } 54 | 55 | interface PostmanCollection { 56 | info: { 57 | name: string; 58 | description?: string; 59 | version?: string; 60 | }; 61 | item: (PostmanItem | PostmanItemGroup)[]; 62 | variable?: PostmanVariable[]; 63 | } 64 | 65 | interface ParseResult { 66 | document: Document; 67 | collectionName: string; 68 | } 69 | 70 | export class PostmanDocumentParser { 71 | private buildRequestBlock(item: PostmanItem): Block { 72 | const block: Block = { 73 | requestSeparator: { 74 | text: null, 75 | }, 76 | metadata: [], 77 | comments: [], 78 | request: { 79 | method: item.request.method, 80 | url: item.request.url.raw, 81 | httpVersion: "HTTP/1.1", 82 | headers: [], 83 | body: null, 84 | }, 85 | preRequestScripts: [], 86 | postRequestScripts: [], 87 | responseRedirect: null, 88 | }; 89 | 90 | // Add name and description as comments 91 | if (item.name) { 92 | block.comments.push(`# ${item.name}\n`); 93 | } 94 | if (item.request.description) { 95 | block.comments.push(`# ${item.request.description}\n`); 96 | } 97 | 98 | // Add name as metadata 99 | if (item.name) { 100 | block.metadata.push({ 101 | key: "name", 102 | value: item.name.replace(/\s+/g, "_").toUpperCase(), 103 | }); 104 | } 105 | 106 | // Handle headers 107 | if (item.request.header) { 108 | item.request.header 109 | .filter((header) => !header.disabled) 110 | .forEach((header) => { 111 | if (block.request) { 112 | block.request.headers.push({ 113 | key: header.key, 114 | value: header.value, 115 | }); 116 | } 117 | }); 118 | } 119 | 120 | // Handle request body 121 | if (item.request.body?.mode === "raw" && item.request.body.raw) { 122 | if (block.request) { 123 | block.request.body = item.request.body.raw; 124 | 125 | // Add Content-Type header if not present 126 | const contentType = 127 | item.request.body.options?.raw?.language || "text/plain"; 128 | if ( 129 | !block.request.headers.some( 130 | (h) => h.key.toLowerCase() === "content-type", 131 | ) 132 | ) { 133 | block.request.headers.push({ 134 | key: "Content-Type", 135 | value: this.getContentType(contentType), 136 | }); 137 | } 138 | } 139 | } 140 | 141 | return block; 142 | } 143 | 144 | private getContentType(language: string): string { 145 | switch (language.toLowerCase()) { 146 | case "json": 147 | return "application/json"; 148 | case "xml": 149 | return "application/xml"; 150 | case "javascript": 151 | return "application/javascript"; 152 | default: 153 | return "text/plain"; 154 | } 155 | } 156 | 157 | private processItems( 158 | items: (PostmanItem | PostmanItemGroup)[], 159 | document: Document, 160 | parentPath: string = "", 161 | ): void { 162 | for (const item of items) { 163 | if (this.isItemGroup(item)) { 164 | // Process folder 165 | const newPath = parentPath ? `${parentPath}/${item.name}` : item.name; 166 | if (item.description) { 167 | document.blocks.push({ 168 | requestSeparator: { 169 | text: null, 170 | }, 171 | metadata: [], 172 | comments: [`# Folder: ${newPath}\n`, `# ${item.description}\n`], 173 | request: null, 174 | preRequestScripts: [], 175 | postRequestScripts: [], 176 | responseRedirect: null, 177 | }); 178 | } 179 | this.processItems(item.item, document, newPath); 180 | } else { 181 | // Process request 182 | const block = this.buildRequestBlock(item); 183 | document.blocks.push(block); 184 | } 185 | } 186 | } 187 | 188 | private isItemGroup( 189 | item: PostmanItem | PostmanItemGroup, 190 | ): item is PostmanItemGroup { 191 | return "item" in item; 192 | } 193 | 194 | parse(collection: PostmanCollection): ParseResult { 195 | const document: Document = { 196 | variables: [], 197 | blocks: [], 198 | }; 199 | 200 | // Add collection variables 201 | if (collection.variable) { 202 | collection.variable 203 | .filter((v) => v.enabled !== false) 204 | .forEach((v) => { 205 | document.variables.push({ 206 | key: v.key, 207 | value: v.value, 208 | }); 209 | }); 210 | } 211 | 212 | // Process all requests in the collection 213 | this.processItems(collection.item, document); 214 | 215 | return { 216 | document: document, 217 | collectionName: collection.info.name, 218 | }; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/lib/parser/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as process from "process"; 3 | import chalk from "chalk"; 4 | import yaml from "js-yaml"; 5 | import { fileWalker } from "./../filewalker"; 6 | import { DocumentParser } from "./DocumentParser"; 7 | import { DocumentBuilder } from "./DocumentBuilder"; 8 | import { Diff } from "./Diff"; 9 | import { OpenAPIDocumentParser, OpenAPISpec } from "./OpenAPIDocumentParser"; 10 | import { PostmanDocumentParser } from "./PostmanDocumentParser"; 11 | import { BrunoDocumentParser } from "./BrunoDocumentParser"; 12 | 13 | const OpenAPIParser = new OpenAPIDocumentParser(); 14 | const PostmanParser = new PostmanDocumentParser(); 15 | const BrunoParser = new BrunoDocumentParser(); 16 | 17 | const getOpenAPISpecAsJSON = (filepath: string): OpenAPISpec => { 18 | if (filepath.endsWith(".yaml") || filepath.endsWith(".yml")) { 19 | return yaml.load(fs.readFileSync(filepath, "utf-8")) as OpenAPISpec; 20 | } 21 | return JSON.parse(fs.readFileSync(filepath, "utf-8")) as OpenAPISpec; 22 | }; 23 | 24 | const isAlreadyPretty = async ( 25 | filepath: string, 26 | options: { body: boolean }, 27 | ): Promise<[boolean | null, string, string]> => { 28 | const content = fs.readFileSync(filepath, "utf-8"); 29 | const document = DocumentParser.parse(content); 30 | if (!document) { 31 | return [null, content, ""]; 32 | } 33 | const build = await DocumentBuilder.build(document, options.body); 34 | return [content === build, content, build]; 35 | }; 36 | 37 | const makeFilePretty = async ( 38 | filepath: string, 39 | formatBody: boolean, 40 | ): Promise => { 41 | const content = fs.readFileSync(filepath, "utf-8"); 42 | const document = DocumentParser.parse(content); 43 | // if document is null, that means we had an error parsing the file 44 | if (!document) { 45 | return null; 46 | } 47 | const build = await DocumentBuilder.build(document, formatBody); 48 | const isPretty = content === build; 49 | if (!isPretty) { 50 | fs.writeFileSync(filepath, build, "utf-8"); 51 | } 52 | return isPretty; 53 | }; 54 | 55 | const getStdinContent = async () => { 56 | try { 57 | let content = ""; 58 | 59 | for await (const chunk of process.stdin) { 60 | content += chunk.toString(); 61 | } 62 | 63 | return content; 64 | } catch (error) { 65 | console.error( 66 | chalk.red( 67 | `Error reading stdin: ${error instanceof Error ? error?.message || error : error}`, 68 | ), 69 | ); 70 | 71 | return process.exit(1); 72 | } 73 | }; 74 | 75 | const checkStdin = async (options: { body: boolean; verbose: boolean }) => { 76 | const content = await getStdinContent(); 77 | 78 | const document = DocumentParser.parse(content); 79 | 80 | if (!document) { 81 | console.error(chalk.red("Error parsing input")); 82 | return process.exit(1); 83 | } 84 | 85 | const formattedDocument = await DocumentBuilder.build(document, options.body); 86 | 87 | const isPretty = formattedDocument === content; 88 | 89 | if (isPretty) { 90 | console.log(chalk.green("Input is pretty")); 91 | } else { 92 | console.error(chalk.yellow("Input is not pretty")); 93 | 94 | if (options.verbose) { 95 | Diff(formattedDocument, content); 96 | } 97 | 98 | return process.exit(1); 99 | } 100 | }; 101 | 102 | /** 103 | * Checks the validity of HTTP files in the given directory. 104 | * 105 | * @param {string} dirPath The directory path to check. 106 | * @param {{ verbose: boolean; body: boolean }} options The options to use. 107 | * @param {string[]} extensions An array of file extensions to filter by (e.g., ['.http', '.rest']). 108 | * @returns {CheckedFiles[]} An array of CheckedFiles objects. 109 | */ 110 | export const check = async ( 111 | dirPath: string | null, 112 | options: { verbose: boolean; body: boolean; stdin: boolean }, 113 | extensions: string[] | undefined = undefined, 114 | ): Promise => { 115 | if (options.stdin) { 116 | await checkStdin(options); 117 | return; 118 | } 119 | 120 | if (!dirPath) { 121 | dirPath = process.cwd(); 122 | } 123 | if (!extensions) { 124 | extensions = [".http", ".rest"]; 125 | } 126 | let errorHappened = false; 127 | const files = fileWalker(dirPath, extensions); 128 | for (const file of files) { 129 | const [isPretty, content, build] = await isAlreadyPretty(file, options); 130 | if (isPretty === false) { 131 | console.log(chalk.yellow(`File not pretty: ${file}`)); 132 | if (options.verbose) { 133 | Diff(build, content); 134 | } 135 | } else if (isPretty === null) { 136 | console.log(chalk.red(`Error parsing file: ${file}`)); 137 | errorHappened = true; 138 | } else { 139 | console.log(chalk.green(`Valid file: ${file}`)); 140 | } 141 | } 142 | if (errorHappened) { 143 | process.exit(1); 144 | } 145 | }; 146 | 147 | export const formatStdin = async (formatBody: boolean) => { 148 | const content = await getStdinContent(); 149 | 150 | const document = DocumentParser.parse(content); 151 | 152 | if (!document) { 153 | console.error(chalk.red("Error parsing input")); 154 | return process.exit(1); 155 | } 156 | 157 | const formattedDocument = await DocumentBuilder.build(document, formatBody); 158 | 159 | process.stdout.write(formattedDocument); 160 | }; 161 | 162 | export const format = async ( 163 | dirPath: string | null, 164 | options: { body: boolean; stdin: boolean }, 165 | extensions: string[] | undefined = undefined, 166 | ): Promise => { 167 | if (options?.stdin) { 168 | await formatStdin(options.body); 169 | return; 170 | } 171 | 172 | if (!dirPath) { 173 | dirPath = process.cwd(); 174 | } 175 | if (!extensions) { 176 | extensions = [".http", ".rest"]; 177 | } 178 | let errorHappened = false; 179 | const files = fileWalker(dirPath, extensions); 180 | for (const file of files) { 181 | const isPretty = await makeFilePretty(file, options.body); 182 | if (isPretty === false) { 183 | console.log(chalk.yellow(`Formatted file: ${file}`)); 184 | } else if (isPretty === null) { 185 | console.log(chalk.red(`Error parsing file: ${file}`)); 186 | errorHappened = true; 187 | } else { 188 | console.log(chalk.green(`Valid file: ${file}`)); 189 | } 190 | } 191 | if (errorHappened) { 192 | process.exit(1); 193 | } 194 | }; 195 | 196 | const convertFromOpenAPI = async (files: string[]): Promise => { 197 | for (const file of files) { 198 | const json = getOpenAPISpecAsJSON(file); 199 | const { documents, serverUrls } = OpenAPIParser.parse(json); 200 | serverUrls.forEach(async (serverUrl, index) => { 201 | const build = await DocumentBuilder.build(documents[index]); 202 | const outputFilename = file.replace(/\.[^/.]+$/, `.${serverUrl}.http`); 203 | fs.writeFileSync(outputFilename, build, "utf-8"); 204 | console.log( 205 | chalk.green( 206 | `Converted OpenAPI spec file: ${file} --> ${outputFilename}`, 207 | ), 208 | ); 209 | }); 210 | } 211 | }; 212 | 213 | const convertFromPostman = async (files: string[]): Promise => { 214 | for (const file of files) { 215 | const json = JSON.parse(fs.readFileSync(file, "utf-8")); 216 | const { document } = PostmanParser.parse(json); 217 | const build = await DocumentBuilder.build(document); 218 | const outputFilename = file.replace(/\.[^/.]+$/, ".http"); 219 | fs.writeFileSync(outputFilename, build, "utf-8"); 220 | console.log( 221 | chalk.green( 222 | `Converted PostMan Collection file: ${file} --> ${outputFilename}`, 223 | ), 224 | ); 225 | } 226 | }; 227 | 228 | const convertFromBruno = async (files: string[]): Promise => { 229 | const { documents, environmentNames, collectionName } = BrunoParser.parse( 230 | files[0], 231 | ); 232 | environmentNames.forEach(async (envName, idx) => { 233 | const build = await DocumentBuilder.build(documents[idx]); 234 | const outputFilename = `${collectionName}.${envName}.http`; 235 | fs.writeFileSync(outputFilename, build, "utf-8"); 236 | console.log( 237 | chalk.green( 238 | `Converted Bruno collection: ${files[0]} --> ${outputFilename}`, 239 | ), 240 | ); 241 | }); 242 | }; 243 | 244 | const invalidFormat = (t: "src" | "dest", format: string): void => { 245 | console.log(chalk.red(`Invalid ${t} format ${format}.`)); 246 | process.exit(1); 247 | }; 248 | 249 | export const convert = async ( 250 | options: { from: string; to: string }, 251 | files: string[], 252 | ): Promise => { 253 | switch (options.from) { 254 | case "openapi": 255 | switch (options.to) { 256 | case "http": 257 | await convertFromOpenAPI(files); 258 | break; 259 | default: 260 | invalidFormat("dest", options.to); 261 | } 262 | break; 263 | case "postman": 264 | switch (options.to) { 265 | case "http": 266 | await convertFromPostman(files); 267 | break; 268 | default: 269 | invalidFormat("dest", options.to); 270 | } 271 | break; 272 | case "bruno": 273 | switch (options.to) { 274 | case "http": 275 | await convertFromBruno(files); 276 | break; 277 | default: 278 | invalidFormat("dest", options.to); 279 | } 280 | break; 281 | default: 282 | invalidFormat("src", options.from); 283 | } 284 | }; 285 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "strict": true, 5 | "outDir": "./dist", 6 | "rootDir": ".", 7 | "skipLibCheck": true, 8 | "moduleResolution": "node", 9 | "target": "esnext", 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src/*.ts", "src/**/*.ts"], 14 | "exclude": ["dist", "node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /web/.envrc: -------------------------------------------------------------------------------- 1 | layout node 2 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /web/.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /web/.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | truthy: 5 | check-keys: false 6 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | ## Developing 2 | 3 | Once you've created a project and installed dependencies with `bun install --frozen-lockfile`. 4 | 5 | ```bash 6 | bun run dev 7 | 8 | # or start the server and open the app in a new browser tab 9 | bun run dev -- --open 10 | ``` 11 | 12 | ## Building 13 | 14 | To create a production version of your app: 15 | 16 | ```bash 17 | bun run build 18 | ``` 19 | 20 | You can preview the production build with `bun run preview`. 21 | -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import svelte from 'eslint-plugin-svelte'; 3 | export default [prettier, ...svelte.configs.prettier]; 4 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "format": "prettier --write .", 14 | "lint": "prettier --check . && eslint ." 15 | }, 16 | "devDependencies": { 17 | "@eslint/compat": "^1.2.7", 18 | "@eslint/js": "^9.22.0", 19 | "@sveltejs/adapter-static": "^3.0.8", 20 | "@sveltejs/kit": "^2.19.0", 21 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 22 | "@tailwindcss/vite": "^4.0.13", 23 | "@types/prismjs": "1.26.5", 24 | "daisyui": "5.0.2", 25 | "eslint": "^9.22.0", 26 | "eslint-config-prettier": "^10.1.1", 27 | "eslint-plugin-svelte": "^3.1.0", 28 | "globals": "^16.0.0", 29 | "prettier": "^3.5.3", 30 | "prettier-plugin-svelte": "^3.3.3", 31 | "prettier-plugin-tailwindcss": "^0.6.11", 32 | "prismjs": "1.30.0", 33 | "svelte": "^5.23.0", 34 | "svelte-check": "^4.1.5", 35 | "tailwindcss": "^4.0.13", 36 | "typescript": "^5.8.2", 37 | "typescript-eslint": "^8.26.1", 38 | "vite": "^6.2.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin 'daisyui' { 3 | themes: dark --default; 4 | } 5 | 6 | div.code-toolbar { 7 | position: relative; 8 | display: grid; 9 | } 10 | div.code-toolbar > .toolbar { 11 | position: absolute; 12 | z-index: 10; 13 | top: -16px; 14 | right: -16px; 15 | transition: opacity 0.3s ease-in-out; 16 | opacity: 1; 17 | } 18 | div.code-toolbar > .toolbar > .toolbar-item { 19 | display: inline-block; 20 | } 21 | div.code-toolbar > .toolbar > .toolbar-item > a { 22 | cursor: pointer; 23 | } 24 | /* icon copy */ 25 | div.code-toolbar > .toolbar > .toolbar-item > button { 26 | font-size: 2rem; 27 | color: #e6caa8; 28 | } 29 | /* color of "Copied!" */ 30 | .copy-to-clipboard-button[data-copy-state~='copy-success'] > span { 31 | color: #10b981; 32 | } 33 | 34 | /* default item color */ 35 | div.code-toolbar > .toolbar > .toolbar-item { 36 | color: #e6caa8; 37 | } 38 | -------------------------------------------------------------------------------- /web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /web/src/lib/HeadComponent.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | {data.title} 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {@render children()} 7 | -------------------------------------------------------------------------------- /web/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | kulala-fmt 52 |

kulala-fmt

53 |

An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡.

54 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |

Install ⚡

64 |

Install kulala-fmt using ...

65 | 71 |
72 |
npm install -g @mistweaverco/kulala-fmt
77 |
78 |
79 |
yarn add --global @mistweaverco/kulala-fmt
84 |
85 |
86 |
bun install -g @mistweaverco/kulala-fmt
91 |
92 |
93 |
pnpm install -g @mistweaverco/kulala-fmt
98 |
99 |

100 | 103 |

104 |
105 |
106 |
107 |
108 |
109 |
110 |

Configure 🔧

111 |

112 | Configure kulala-fmt using a simple configuration file kulala-fmt.yaml. 113 |

114 |
115 |
kulala-fmt init
120 |
121 | 140 |

141 | 144 |

145 |
146 |
147 |
148 |
149 |
150 |
151 |

Usage 🐆

152 |

Run kulala-fmt to format your .http files.

153 |
kulala-fmt format /tmp/test.http test.http
156 |

157 | 160 |

161 |
162 |
163 |
164 |
165 |
166 |
167 |

Get involved 📦

168 |

kulala-fmt is open-source and we welcome contributions.

169 |

170 | View the code, 171 | and/or check out the docs. 172 |

173 |
174 |
175 |
176 | -------------------------------------------------------------------------------- /web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/web/static/favicon.png -------------------------------------------------------------------------------- /web/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/web/static/logo.png -------------------------------------------------------------------------------- /web/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/static/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "defaults": { 6 | "type": "object", 7 | "properties": { 8 | "http_method": { 9 | "type": "string", 10 | "enum": [ 11 | "GET", 12 | "POST", 13 | "PUT", 14 | "DELETE", 15 | "PATCH", 16 | "HEAD", 17 | "OPTIONS", 18 | "TRACE", 19 | "CONNECT", 20 | "WEBSOCKET", 21 | "GRAPHQL" 22 | ] 23 | }, 24 | "http_version": { 25 | "type": "string", 26 | "enum": [ 27 | "HTTP/1.0", 28 | "HTTP/1.1", 29 | "HTTP/2.0", 30 | "HTTP/3.0" 31 | ] 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | 8 | kit: { 9 | adapter: adapter() 10 | }, 11 | 12 | extensions: ['.svelte'] 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "target": "esnext" 14 | } 15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 | // 18 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 19 | // from the referenced tsconfig.json - TypeScript does not merge them in 20 | } 21 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), tailwindcss()] 7 | }); 8 | --------------------------------------------------------------------------------